Today, we're diving into an essential C# and .NET programming aspect: understanding and managing resources. So, let's get started.

Managed Resources

In the .NET world, managed resources are objects the .NET runtime's garbage collector (GC) can handle. Whenever we use the new keyword to create an object in C#, we deal with a managed resource. The beauty of managed resources lies in how the GC handles them. It automatically reclaims the memory occupied by these resources when they're no longer in use, keeping our applications optimized and memory-efficient.

Unmanaged Resources

On the other hand, unmanaged resources are resources not directly controlled by the .NET runtime. These resources include file handles, network sockets, database connections, and more. The GC won't touch these resources, and it's our responsibility to clean them up. Leaving unmanaged resources undisposed can lead to significant issues, including memory leaks, resource contention, and application slowdowns.

Disposing of Unmanaged Resources

To help us manage unmanaged resources, C# provides the IDisposable interface, which declares a Dispose method. Implementing this interface in classes that use unmanaged resources allows us to provide instructions for freeing those resources when they're no longer needed. But let's not forget about Finalizers. A Finalizer (also known as a destructor) is a method automatically invoked by the runtime to perform clean-up operations on an object before garbage is collected.

Here's an example of a UnitOfWork class that implements IDisposable and includes a Finalizer to manage a database connection:

public interface IUnitOfWork : IDisposable
{
    // ... other members ...
    void Save();
}

public class UnitOfWork : IUnitOfWork
{
    private CustomDbContext _context;
    private bool _disposed = false;

    public UnitOfWork(CustomDbContext context)
    {
        _context = context;
    }

    // ... other members ...

    public void Save()
    {
        _context.SaveChanges();
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed)
            return;

        if (disposing)
        {
            // Dispose managed resources.
            _context.Dispose();
            _context = null;
        }

        // Free any unmanaged objects here. 
        _disposed = true;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    ~UnitOfWork()
    {
        Dispose(false);
    }
}

CustomDbContext is a managed resource that encapsulates several unmanaged resources, such as a database connection. When the Dispose method is explicitly called, it disposes of the CustomDbContext, which in turn closes the database connection. If Dispose is not called, the Finalizer will eventually run and close the connection. The GC.SuppressFinalize(this); call in the Dispose method tells the GC that the object was cleaned up properly and does not need to be finalized.

In addition to IDisposable and Finalizer, C# also offers the using statement. A using statement ensures that the Dispose method is called on an IDisposable object as soon as it falls out of scope, even if an exception is thrown. This guarantees unmanaged resources are released:

using (IUnitOfWork unitOfWork = new UnitOfWork(new CustomDbContext()))
{
    // ... Perform operations with unitOfWork ...
}

In this example, the UnitOfWork is instantiated inside a using statement. Its Dispose method will automatically be called when the using block ends, regardless of whether it completes normally or throws an exception. This ensures that the database connection is properly closed and cleaned up, even in the event of an error.

What if we want to integrate our UnitOfWork with Microsoft's DI in a Minimal API? You'll be glad to know you won't have to worry about its disposal.

Let's upgrade our understanding by illustrating how you can leverage this pattern within Microsoft's latest addition to the ASP.NET Core family - the Minimal APIs.

Meet Minimal APIs

Minimal APIs, introduced in .NET 6, have taken the stage with a simpler and leaner approach to building APIs. With fewer components and setups, you can get your API up and running quickly. Let's see how we can set up our UnitOfWork with dependency injection in a Minimal API:

var builder = WebApplication.CreateBuilder(args);

// Registering the DbContext and UnitOfWork in DI
builder.Services.AddDbContext<CustomDbContext>(options => 
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();

var app = builder.Build();

// An endpoint that uses IUnitOfWork
app.MapGet("/endpoint", (IUnitOfWork unitOfWork) => 
{
    // ... use the unitOfWork here ...
    return Results.Ok();
});

app.Run();

We're defining an endpoint that uses our UnitOfWork. In the endpoint's lambda expression, we're declaring IUnitOfWork unitOfWork as a parameter - a straightforward example of parameter-based dependency injection. ASP.NET Core's routing system automatically resolves this dependency from the configured services and injects it into our endpoint. That's less for us to worry about!

It's worth noting that in Minimal APIs, the UnitOfWork will be automatically disposed at the end of the request because it's configured with AddScoped. With the right design and service lifetimes, we rarely need to manually manage the lifecycle of such dependencies within the scope of a method. Isn't that a relief?

So, once the request is completed, UnitOfWork will automatically get disposed of and its Dispose method coming from the IDisposable interface will be called, cleaning up the used resources.

Key Takeaways

  • The GC automatically cleans up managed resources, while unmanaged resources need explicit handling.
  • Leaving unmanaged resources undisposed can cause serious application issues.
  • The IDisposable interface and using statements are tools provided by C# to manage unmanaged resources effectively.
  • Finalizers statements provide a fallback option for cleanup but should not be relied upon due to their non-deterministic timing.
  • Understanding and effectively managing resources in a .NET application is key to developing high-performing, robust applications.