Scrutor Makes .NET DI Easy

🔍 What Is Scrutor? A Simple Way to Register Services in .NET

In .NET applications, Dependency Injection (DI) helps keep code clean, flexible, and easy to test. The default DI container in .NET is Microsoft.Extensions.DependencyInjection (MS.DI), which provides only the essential features required to register and resolve dependencies.

While MS.DI is effective for small applications, manually registering each service can become difficult and time-consuming in larger projects. It often leads to repetitive code and increases the chances of mistakes.

That’s where Scrutor comes in as a lightweight but powerful NuGet package that enhances .NET’s built-in DI with assembly scanning, attribute filtering, and even decorator support for auto service registrations, making it much easier to manage dependencies — especially as your application grows.

In this article, you’ll learn how to use Scrutor to simplify service registration and make your .NET applications easier to manage.

💡 Basic Usage: Scan and Register Services

Instead of doing this manually:

1services.AddScoped<<interface>>, <class>>();

You can write:

1services.Scan(scan => scan
2 .FromExecutingAssembly()
3 .AddClasses()
4 .AsImplementedInterfaces()
5 .WithScopedLifetime());

Boom 💥 — all classes are auto-registered.


✅ PreRequisites

Before we dive into the blog, it’s helpful to have a basic understanding of the following .NET concepts:

  • How services are registered in .NET using AddScoped, AddTransient with MS.DI
  • The basics of constructor injection

If you're good with above concepts, you're ready to fly 🚀.
If not, I recommend brushing up on:

Now, Let’s say you have the following service implementations in your code:

1public interface IUserService
2{
3 void PrintName();
4}
5
6public class UserService : IUserService
7{
8 public void PrintName() => Console.WriteLine("Chris Jordan");
9}
10
11// ---------------------------------------
12public interface IEmailService
13{
14 void Send();
15}
16
17public class EmailMessageService : IEmailService
18{
19 public void Send() => Console.WriteLine("Sending email...");
20}
21
22// -----------------------------------------
23
24public interface ICommonUtils
25{
26 void ConvertToInt();
27 void ConvertToDouble();
28}
29
30public class CommonUtils : ICommonUtils
31{
32 public void ConvertToInt() => Console.WriteLine("Converted to number...");
33 public void ConvertToDouble() => Console.WriteLine("Converted to double...");
34}

✅ Getting Started

Install via NuGet:

1dotnet add package Microsoft.Extensions.DependencyInjection
2dotnet add package Scrutor

1. Scan and Register Services by Convention (Assembly Scanning)

UseCase : Auto-register all services.

✅ With Scrutor:

1services.Scan(scan => scan
2 .FromAssemblyOf<IEmailService>() // or FromExecutingAssembly()
3 .AddClasses()
4 .AsImplementedInterfaces());
5 .WithScopedLifetime();

All services is now auto-wired into DI.

🚫 With MS.DI:

1services.AddScoped<IUserService, UserService>();
2services.AddScoped<IEmailService, EmailService>();
3services.AddScoped<ICommonUtils, CommonUtils>();

2. Register Only Classes Matching Custom Filters

UseCase : Auto-register only classes following a naming convention like *Service.

1services.Scan(scan => scan
2 .FromAssemblyOf<IUserService>()
3 .AddClasses(c => c.Where(type => type.Name.EndsWith("Service")))
4 .AsImplementedInterfaces()
5 .WithTransientLifetime());

✅ With scrutor all services will now be auto-wired into the DI container, except CommonUtils, as it does not meet the registration criteria.

🚫 With MS.DI : No filtering capability. You'd need reflection + custom logic.

📋 Here are various ways you can apply filtering in scrutor.

FeatureMethod(s)
Filter by type/interface.AssignableTo()
Filter by namespace.InNamespace(...)
Filter by attribute.WithAttribute(), .WithoutAttribute()
Custom predicate.Where(t => /_ your condition _/)
Register as interface(s).AsImplementedInterfaces()
Register class as self.AsSelf()

3. Register Open Generics Automatically

An open generic is a type that has one or more unassigned generic parameters like IGenericRepository<T>.

UseCase :You don't know what T is yet, and you want to automatically register all implementations like below:

1IGenericRepository<Customer>
2IGenericRepository<Order>

Let's say you have a generic repository code like below 👇

1public interface IGenericRepository<T> where T : class
2{
3 T GetById(int id);
4 IEnumerable<T> GetAll();
5}
6
7public class Customer
8{
9 public int Id;
10 public string Name;
11}
12
13public class Order
14{
15 public int Id;
16 public double Total;
17}
18
19public class CustomerRepository : IGenericRepository<Customer>
20{
21 public Customer GetById(int id) => new Customer { Id = id, Name = "Chris Jordan" };
22 public IEnumerable<Customer> GetAll() => new[] { new Customer { Id = 1, Name = "Chris Jordan" } };
23}
24
25public class OrderRepository : IGenericRepository<Order>
26{
27 public Order GetById(int id) => new Order { Id = id, Total = 99.99 };
28 public IEnumerable<Order> GetAll() => new[] { new Order { Id = 1, Total = 99.99 } };
29}

✅ With Scrutor (📦 Magic Happens Here!)

1 services.Scan(scan => scan
2 .FromAssemblyOf<IGenericRepository<Customer>>()
3 .AddClasses(c => c.AssignableTo(typeof(IGenericRepository<>)))
4 .AsImplementedInterfaces()
5 .WithScopedLifetime());

Scrutor scans all classes implementing IGenericRepository<T> (with any T) and registers them with their corresponding interface.

🚫 With MS.DI:

1services.AddScoped<IGenericRepository<Customer>, CustomerRepository>();
2services.AddScoped<IGenericRepository<Order>, OrderRepository>();
3// Add one for every new entity!

This becomes tedious and error-prone as your app grows.


4. Avoid Registering Abstract Classes and Interfaces

UseCase : Register only concrete and non abstract classes

Now imagine your assembly contains this:

1public interface IService { }
2
3public abstract class BaseService : IService
4{
5 public abstract void DoWork();
6}
7
8public class RealService : BaseService
9{
10 public override void DoWork() => Console.WriteLine("Working...");
11}
1services.Scan(scan => scan
2 .FromAssemblyOf<IService>()
3 .AddClasses(c => c.Where(type => type.Name.EndsWith("Service"))));

Without filtering, Scrutor might try to register:

  • BaseService (Abstract — can't be instantiated)
  • RealService (Concrete — good to go)

✅ With .AddClasses() Scrutor automatically skips abstract classes and interfaces during scanning.

🚫 With MS.DI:

You’d need to write extra logic to check type.IsAbstract or type.IsInterface.


5. Custom Attribute-based Filtering

UseCase : Register only services marked with [AutoRegister] for better control.

Step 1 : Create an Attribute

1[AttributeUsage(AttributeTargets.Class)]
2public class AutoRegisterAttribute : Attribute { }

Step 2 : Use It

1[AutoRegister]
2public class EmailService : IEmailService { }

✅ With Scrutor:

1services.Scan(scan => scan
2 .FromExecutingAssembly()
3 .AddClasses(c => c.WithAttribute<AutoRegisterAttribute>())
4 .AsImplementedInterfaces());

Now only classes with [AutoRegister] attribute will be registered!

🚫 With MS.DI:

No support. You’d need to write your own reflection + logic.


In the next part, we’ll dive deeper into Scrutor exploring how to work with Multiple Assemblies and implement the powerful Decorator Pattern using clean and maintainable best practices.

Stay tuned—the real magic is just around the corner!