Low-Fat vs High-Fat NuGet Packages: A Developer's Decision Guide

• .NET, NuGet, Architecture, Best Practices, Validation, Dependency Injection, Logging, Software Development, Presentation • 7 min read

Welcome to 2025! As we kick off the new year with fresh perspectives on software development, it’s time to examine one of the most fundamental decisions we make as .NET developers: when to leverage built-in framework features versus external NuGet packages.

This comprehensive guide stems from a recent Coding Night New Zealand session where we explored the concept of “low-fat” (built-in) versus “high-fat” (external package) implementations across three critical areas: validation, dependency injection, and logging.

The Low-Fat vs High-Fat Analogy

Just like we make conscious choices about our diet, especially after the holiday season, we should be equally mindful about the “nutritional value” of our code dependencies.

  • Low-Fat (Built-in): Framework features that come out of the box with .NET
  • High-Fat (External): Third-party NuGet packages with additional features and complexity

The analogy isn’t about labeling external packages as “bad” – it’s about making informed decisions based on your application’s actual needs.

Three Key Areas Compared

1. Validation: Built-in vs Fluent Validation

The Low-Fat Approach: IValidatableObject

.NET provides built-in validation through System.ComponentModel.DataAnnotations:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class Customer : IValidatableObject
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (string.IsNullOrEmpty(FirstName))
            yield return new ValidationResult("First name is required");

        if (string.IsNullOrEmpty(LastName))
            yield return new ValidationResult("Last name is required");

        if (Age < 18)
            yield return new ValidationResult("Customer must be at least 18 years old");
    }
}

Advantages:

  • No additional dependencies
  • Part of the .NET framework
  • Straightforward implementation

The High-Fat Approach: FluentValidation

Using FluentValidation provides a more expressive API:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class CustomerValidator : AbstractValidator<Customer>
{
    public CustomerValidator()
    {
        RuleFor(x => x.FirstName)
            .NotEmpty()
            .WithMessage("First name is required");

        RuleFor(x => x.LastName)
            .NotEmpty()
            .WithMessage("Last name is required");

        RuleFor(x => x.Age)
            .GreaterThanOrEqualTo(18)
            .WithMessage("Customer must be at least 18 years old");
    }
}

Advantages:

  • More expressive syntax
  • Better separation of concerns
  • Rich feature set for complex validation scenarios

Considerations:

  • Additional dependency
  • FluentValidation licensing has changed (version 8+ requires licensing for commercial use)

2. Dependency Injection: Built-in vs Autofac

The Low-Fat Approach: Microsoft.Extensions.DependencyInjection

1
2
3
4
5
6
7
8
// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Register services
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddScoped<IShoppingCartService, ShoppingCartService>();

var app = builder.Build();

Advantages:

  • Built into .NET Core/5+
  • Zero additional dependencies
  • Sufficient for most scenarios

The High-Fat Approach: Autofac

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Using Autofac with modules
public class ServiceModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<ProductService>()
               .As<IProductService>()
               .InstancePerLifetimeScope();

        builder.RegisterType<ShoppingCartService>()
               .As<IShoppingCartService>()
               .InstancePerLifetimeScope();
    }
}

// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
builder.Host.ConfigureContainer<ContainerBuilder>(containerBuilder =>
{
    containerBuilder.RegisterModule<ServiceModule>();
});

When Autofac Makes Sense:

  • Legacy .NET Framework to .NET Core migrations
  • Complex modular architectures
  • Advanced DI scenarios requiring Autofac-specific features

When Built-in DI Suffices:

  • Greenfield .NET projects
  • Standard dependency injection needs
  • Preference for minimal external dependencies

3. Logging: Built-in vs Serilog

The Low-Fat Approach: ILogger with Azure Monitor

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Program.cs - Using built-in logging with Azure Monitor
builder.Services.AddLogging();
builder.Services.AddApplicationInsightsTelemetry();

// In your controller/service
public class CustomerController : ControllerBase
{
    private readonly ILogger<CustomerController> _logger;

    public CustomerController(ILogger<CustomerController> logger)
    {
        _logger = logger;
    }

    [HttpPost]
    public IActionResult CreateCustomer([FromBody] Customer customer)
    {
        _logger.LogInformation("Creating customer: {@Customer}", customer);
        // Business logic here
        return Ok("Customer created successfully");
    }
}

Advantages:

  • Built into .NET
  • Direct integration with Azure Monitor/Application Insights
  • Minimal configuration required

The High-Fat Approach: Serilog

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Program.cs - Serilog configuration
builder.Host.UseSerilog((context, config) =>
{
    config.ReadFrom.Configuration(context.Configuration)
          .WriteTo.Console()
          .WriteTo.ApplicationInsights(connectionString, TelemetryConverter.Traces);
});

// Rich structured logging capabilities
Log.Information("Creating customer {FirstName} {LastName} aged {Age}", 
    customer.FirstName, customer.LastName, customer.Age);

Advantages:

  • Rich structured logging
  • Extensive sink ecosystem
  • Powerful configuration options
  • Enhanced formatting and filtering

Trade-offs:

  • Additional dependencies (Serilog.Extensions.AspNetCore, various sinks)
  • More complex configuration
  • Potentially overkill for simple logging needs

Decision-Making Framework

When choosing between low-fat and high-fat approaches, consider these factors:

1. Maintainability

  • Built-in: Supported by Microsoft, updates with .NET releases
  • Third-party: Requires tracking updates, compatibility checking, potential breaking changes

2. Complexity

  • Built-in: Simpler, covers basic use cases
  • Third-party: Advanced features for complex scenarios

3. Performance

  • Built-in: Optimized for .NET runtime
  • Third-party: May introduce minimal overhead

4. Dependencies

  • Built-in: Zero additional references
  • Third-party: Additional packages to manage (dependency hell risk)

5. Community Support

  • Built-in: Microsoft backing
  • Third-party: Varies by maintainer and community size

6. Features & Flexibility

  • Built-in: Core functionality
  • Third-party: Rich feature sets for advanced requirements

7. Cost & Licensing

  • Built-in: Free as part of .NET
  • Third-party: Check licensing terms (some require commercial licenses)

8. Security

  • Built-in: Microsoft security assurance
  • Third-party: Security depends on maintainer practices

9. Development Speed

  • Built-in: May require more effort for complex functionality
  • Third-party: Faster development with feature-rich libraries

10. Long-term Vision

  • Built-in: Stable, less likely to be deprecated
  • Third-party: Risk of abandonment (verify active maintenance)

Software Development Principles to Guide Decisions

YAGNI (You Ain’t Gonna Need It)

Don’t over-engineer for future requirements that may never materialize. Focus on current, well-defined needs.

Example: Don’t implement complex caching, scaling, or advanced logging if you’re building an MVP for market validation.

KISS (Keep It Simple, Stupid)

Prioritize simplicity over complexity. Choose the simplest solution that meets your requirements.

DRY (Don’t Repeat Yourself)

Favor code reusability and avoid duplication, but don’t sacrifice simplicity for abstract perfection.

Product Mindset: The Build-Ship-Tweak Cycle

Adopting a product mindset helps inform these decisions:

  1. Think - Identify actual requirements
  2. Build - Implement only what’s needed (MVP approach)
  3. Ship - Deliver quickly to reduce cost and risk
  4. Tweak - Iterate based on real-world feedback

Key insight: Faster shipping reduces costs and product risk. The operational cost curve shows that the longer you take to ship, the higher your costs become before you start generating revenue.

Practical Recommendations

Start with Built-in, Upgrade When Needed

  1. Begin with low-fat implementations for new projects
  2. Monitor pain points during development
  3. Upgrade to third-party libraries when built-in features become limiting
  4. Document the decision rationale for future reference

Consider Your Context

  • Greenfield projects: Favor built-in solutions
  • Legacy migrations: External packages might ease transition
  • Enterprise requirements: May justify additional complexity
  • Startup MVP: Keep it simple with built-in features

Team Considerations

  • Team expertise: Leverage existing knowledge
  • Maintenance capacity: Consider long-term support implications
  • Standardization: Align with organizational practices

Real-World Examples from the Session

The demonstration showed three identical applications implementing the same functionality:

  1. Validation: Both IValidatableObject and FluentValidation successfully validated customer data
  2. Dependency Injection: Both built-in DI and Autofac properly resolved and injected services
  3. Logging: Both ILogger and Serilog successfully logged to console and Azure Application Insights

Key takeaway: Both approaches worked for the basic requirements. The choice depends on your specific needs, team preferences, and architectural constraints.

Conclusion

The “low-fat vs high-fat” analogy serves as a useful mental model for making dependency decisions in .NET applications. There’s no universally right answer – the best choice depends on your specific context, requirements, and constraints.

Remember:

  • Question your assumptions and challenge existing practices
  • Discuss decisions with your team
  • Consider the principles: YAGNI, KISS, and DRY
  • Think with a product mindset: build what you need now, not what you might need later
  • Keep learning and stay open to different approaches

The goal isn’t to avoid external packages entirely but to make conscious, informed decisions about when the additional complexity is justified by genuine business or technical requirements.


Resources

What’s your experience with choosing between built-in and external packages? Share your thoughts and decision-making criteria in the comments below!

Comments & Discussion

Join the conversation! Share your thoughts and connect with other readers.