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.