Tag: dotnet

  • Mastering Dependency Injection in ASP.NET Core: A Complete Guide

    Imagine you are building a modern car. If you weld the engine directly to the chassis, you might have a functional vehicle for a while. However, the moment you need to upgrade the engine, repair a piston, or swap it for an electric motor, you realize you have a massive problem. You have to tear the entire car apart because the components are “tightly coupled.”

    In software development, particularly within the ASP.NET Core ecosystem, we face the same dilemma. Without proper architecture, our classes become tightly coupled to their dependencies. This makes our code difficult to test, impossible to maintain, and a nightmare to extend. This is where Dependency Injection (DI) comes to the rescue.

    In this comprehensive guide, we will dive deep into the world of Dependency Injection in ASP.NET Core. Whether you are a beginner looking to understand the basics or an intermediate developer seeking to master service lifetimes and advanced patterns, this article will provide the technical depth you need to build professional-grade applications.

    What is Dependency Injection?

    Dependency Injection is a design pattern used to achieve Inversion of Control (IoC) between classes and their dependencies. In simpler terms, instead of a class creating its own “helper” objects (like database contexts, logging services, or email providers), those objects are “injected” into the class from the outside.

    Think of it like a restaurant. A chef (the class) needs a sharp knife (the dependency). In a poorly designed system, the chef would have to stop cooking, go to the blacksmith, and forge a knife themselves. In a DI-based system, the restaurant manager (the DI Container) simply hands the chef a knife when they start their shift.

    The Dependency Inversion Principle

    DI is the practical implementation of the Dependency Inversion Principle, one of the five SOLID principles of object-oriented design. It states:

    • High-level modules should not depend on low-level modules. Both should depend on abstractions.
    • Abstractions should not depend on details. Details should depend on abstractions.

    Why ASP.NET Core DI Matters

    Unlike earlier versions of the .NET Framework, where DI was often an afterthought or required third-party libraries like Autofac or Ninject, ASP.NET Core has DI built into its very core. It is a first-class citizen. Every part of the framework—from Middleware and Controllers to Identity and Entity Framework—relies on it.

    By using DI, you gain:

    • Maintainability: Changes in one part of the system don’t break everything else.
    • Testability: You can easily swap real services for “Mock” services during unit testing.
    • Readability: Dependencies are clearly listed in the constructor of a class.
    • Configuration Management: Centralized control over how objects are created and shared.

    Understanding the Service Collection and Service Provider

    To implement DI, ASP.NET Core uses two primary components:

    1. IServiceCollection: A list of service descriptors where you “register” your dependencies during application startup (usually in Program.cs).
    2. IServiceProvider: The engine that actually creates and manages the instances of the services based on the registrations.

    Deep Dive: Service Lifetimes

    One of the most critical concepts to master in ASP.NET Core DI is Service Lifetimes. This determines how long a created object lives before it is disposed of. Choosing the wrong lifetime is a leading cause of memory leaks and bugs.

    1. Transient Services

    Transient services are created every time they are requested. Each request for the service results in a new instance. This is the safest bet for lightweight, stateless services.

    // Registration in Program.cs
    builder.Services.AddTransient<IMyService, MyService>();
    

    Use case: Simple utility classes, calculators, or mappers that don’t hold state.

    2. Scoped Services

    Scoped services are created once per client request (e.g., within the lifecycle of a single HTTP request). Within the same request, the service instance is shared across different components.

    // Registration in Program.cs
    builder.Services.AddScoped<IUserRepository, UserRepository>();
    

    Use case: Entity Framework Database Contexts (DbContext). You want the same database connection shared across your repository and your controller during one web request.

    3. Singleton Services

    Singleton services are created the first time they are requested and then every subsequent request uses that same instance. The instance stays alive until the application shuts down.

    // Registration in Program.cs
    builder.Services.AddSingleton<ICacheService, CacheService>();
    

    Use case: In-memory caches, configuration wrappers, or stateful services that must be shared globally.

    Step-by-Step Tutorial: Implementing DI in a Project

    Let’s build a real-world example: A notification system that can send messages via Email or SMS. We want our controller to be able to send notifications without knowing the specifics of how the email is sent.

    Step 1: Define the Abstraction (Interface)

    First, we define what our service does, not how it does it.

    public interface IMessageService
    {
        string SendMessage(string recipient, string content);
    }
    

    Step 2: Create the Implementation

    Now, we create a concrete class that implements our interface.

    public class EmailService : IMessageService
    {
        public string SendMessage(string recipient, string content)
        {
            // In a real app, this would involve SMTP logic
            return $"Email sent to {recipient} with content: {content}";
        }
    }
    

    Step 3: Register the Service

    Go to your Program.cs file and register the service with the DI container. We will use AddScoped for this example.

    var builder = WebApplication.CreateBuilder(args);
    
    // Register our service here
    builder.Services.AddScoped<IMessageService, EmailService>();
    
    var app = builder.Build();
    

    Step 4: Use Constructor Injection

    Finally, we inject the service into our Controller. Note that we depend on the interface, not the class.

    [ApiController]
    [Route("[controller]")]
    public class NotificationController : ControllerBase
    {
        private readonly IMessageService _messageService;
    
        // The DI container provides the instance here automatically
        public NotificationController(IMessageService messageService)
        {
            _messageService = messageService;
        }
    
        [HttpPost]
        public IActionResult Notify(string user, string message)
        {
            var result = _messageService.SendMessage(user, message);
            return Ok(result);
        }
    }
    

    Advanced Scenarios: Keyed Services (New in .NET 8)

    Sometimes, you have multiple implementations of the same interface and you want to choose between them. Before .NET 8, this was cumbersome. Now, we have Keyed Services.

    // Registration
    builder.Services.AddKeyedScoped<IMessageService, EmailService>("email");
    builder.Services.AddKeyedScoped<IMessageService, SmsService>("sms");
    
    // Usage in Controller
    public class MyController(
        [FromKeyedServices("sms")] IMessageService smsService)
    {
        // Use the SMS version here
    }
    

    Common Mistakes and How to Avoid Them

    1. Captive Dependency

    This is the most common “Intermediate” mistake. It happens when a service with a long lifetime (like a Singleton) depends on a service with a short lifetime (like a Scoped service).

    The Problem: Because the Singleton lives forever, it holds onto the Scoped service forever, effectively turning the Scoped service into a Singleton. This can lead to DB context errors and stale data.

    The Fix: Always ensure your dependencies have a lifetime equal to or longer than the service using them. Never inject a Scoped service into a Singleton.

    2. Over-injecting (The Fat Constructor)

    If your constructor has 10+ dependencies, your class is probably doing too much. This is a violation of the Single Responsibility Principle.

    The Fix: Break the large class into smaller, more focused classes.

    3. Using Service Locator Pattern

    Manually calling HttpContext.RequestServices.GetService<T>() inside your methods is known as the Service Locator pattern. It hides dependencies and makes unit testing much harder.

    The Fix: Always prefer Constructor Injection.

    Best Practices for Clean Architecture

    • Register by Interface: Always register services using an interface (IMyService) rather than the concrete class (MyService).
    • Keep Program.cs Clean: If you have dozens of services, create an Extension Method like services.AddMyBusinessServices() to group registrations.
    • Avoid Logic in Constructors: Constructors should only assign injected services to private fields. Avoid complex logic or database calls during object creation.

    Unit Testing with Dependency Injection

    One of the greatest benefits of DI is the ease of testing. Instead of connecting to a live database, you can use a library like Moq to provide a fake version of your service.

    [Fact]
    public void Controller_Should_Call_SendMessage()
    {
        // Arrange
        var mockService = new Mock<IMessageService>();
        var controller = new NotificationController(mockService.Object);
    
        // Act
        controller.Notify("test@example.com", "Hello");
    
        // Assert
        mockService.Verify(s => s.SendMessage(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
    }
    

    Summary and Key Takeaways

    Dependency Injection in ASP.NET Core is not just a feature; it is the foundation of the framework. By mastering DI, you write code that is decoupled, easy to test, and ready for change.

    • Transient: New instance every time. Use for stateless logic.
    • Scoped: Once per HTTP request. Use for Data Contexts.
    • Singleton: Once per application. Use for caching/state.
    • DIP: Always depend on interfaces, not implementations.
    • Avoid Captive Dependencies: Don’t inject Scoped into Singleton.

    Frequently Asked Questions (FAQ)

    1. Can I use third-party DI containers like Autofac?

    Yes. While the built-in container is sufficient for 90% of applications, third-party containers offer advanced features like property injection and assembly scanning. ASP.NET Core makes it easy to swap the default provider.

    2. Is there a performance hit when using DI?

    The overhead of the DI container is negligible for most web applications. The benefits of maintainability and testability far outweigh the microsecond-level cost of service resolution.

    3. What happens if I forget to register a service?

    ASP.NET Core will throw an InvalidOperationException at runtime when it tries to instantiate a class that requires that service. In development, the error message is usually very clear about which service is missing.

    4. Should I use DI in Console Applications too?

    Absolutely. You can set up the Host.CreateDefaultBuilder() in console apps to gain access to the same IServiceCollection and IServiceProvider used in web apps.

    5. Is it okay to use “new” keyword for simple classes?

    Yes. You don’t need to inject everything. Simple “Data Transfer Objects” (DTOs), Entities, and “Value Objects” should usually be instantiated normally using new.

  • Mastering MVVM in .NET MAUI: The Ultimate Developer Guide

    Introduction: The Battle Against Spaghetti Code

    If you have ever built a mobile or desktop application and found yourself staring at a 2,000-line “code-behind” file where UI logic, database calls, and validation are all tangled together like cold spaghetti, you are not alone. This is the “Big Ball of Mud” scenario that haunts developers during maintenance cycles. Every time you fix a bug in the UI, something in the data layer breaks. Every time you want to write a unit test, you realize it is impossible because your logic is physically glued to a button click event.

    Enter .NET MAUI (Multi-platform App UI) and the Model-View-ViewModel (MVVM) architectural pattern. MVVM is not just a “suggestion” in the world of .NET cross-platform development; it is the industry standard. It provides a clean separation of concerns, making your code testable, maintainable, and scalable across Android, iOS, macOS, and Windows.

    In this guide, we will dive deep into MVVM in .NET MAUI. We will move from basic concepts to advanced implementation using the latest Source Generators, Dependency Injection, and the Community Toolkit. Whether you are a beginner looking to build your first app or an intermediate developer seeking to refactor legacy code, this guide is your definitive roadmap.

    Understanding the MVVM Trio: Model, View, and ViewModel

    To master MVVM, we must first understand the three distinct roles that make up the architecture. Think of it like a restaurant: the Chef (Model), the Waiter (ViewModel), and the Customer (View).

    1. The Model (The Data and Logic)

    The Model represents the data structures and the business rules of your application. It knows nothing about the UI. It might be a simple class representing a “Product” or a service that fetches data from a REST API. In our restaurant analogy, the Model is the kitchen and the ingredients.

    2. The View (The Visuals)

    The View is what the user sees and interacts with. In .NET MAUI, this is typically defined in XAML (Extensible Application Markup Language). The View should be “dumb”—it should only care about how things look, not how the data is processed. The View is the customer’s table and the menu.

    3. The ViewModel (The Orchestrator)

    The ViewModel is the bridge. It acts as a converter that changes Model data into a format the View can easily display. It handles user input (via Commands) and notifies the View when data changes (via Data Binding). Crucially, the ViewModel has no reference to the View. It doesn’t know if it’s running on an iPhone or a Windows PC. The ViewModel is the waiter who takes the order and brings the food.

    Setting Up Your .NET MAUI Environment for MVVM

    Before we write code, we need the right tools. While you can implement MVVM manually by implementing INotifyPropertyChanged, modern developers use the CommunityToolkit.Mvvm library. It uses C# Source Generators to write the repetitive “boilerplate” code for you.

    Step 1: Install the NuGet Package

    Open your NuGet Package Manager or use the CLI to install:

    dotnet add package CommunityToolkit.Mvvm

    Step 2: Organize Your Folder Structure

    A clean project structure is vital for SEO and maintainability. Create the following folders in your .NET MAUI project:

    • Models: For your data objects.
    • ViewModels: For your logic classes.
    • Views: For your XAML pages.
    • Services: For API or Database interaction.

    Implementing the Model

    Let’s build a simple “Task Manager” application. We start with our Model. This is a plain old C# object (POCO).

    
    namespace MauiMvvmGuide.Models
    {
        public class TodoItem
        {
            public string Title { get; set; }
            public bool IsCompleted { get; set; }
        }
    }
                

    Notice that this class is simple. It doesn’t inherit from anything or use any special MAUI namespaces. This makes it extremely easy to unit test or move to a different project later.

    The Power of the ViewModel (Modern Approach)

    In the old days, you had to write 10 lines of code for every property to notify the UI of a change. With the Community Toolkit, we use the [ObservableProperty] attribute. The toolkit’s source generator will automatically create the Title and IsCompleted properties with all the notification logic included.

    
    using CommunityToolkit.Mvvm.ComponentModel;
    using CommunityToolkit.Mvvm.Input;
    using MauiMvvmGuide.Models;
    using System.Collections.ObjectModel;
    
    namespace MauiMvvmGuide.ViewModels
    {
        // Partial class is required for Source Generators to work
        public partial class MainViewModel : ObservableObject
        {
            [ObservableProperty]
            string newTaskTitle;
    
            // ObservableCollection automatically updates the UI when items are added/removed
            public ObservableCollection<TodoItem> Tasks { get; } = new();
    
            [RelayCommand]
            void AddTask()
            {
                if (string.IsNullOrWhiteSpace(NewTaskTitle))
                    return;
    
                Tasks.Add(new TodoItem { Title = NewTaskTitle, IsCompleted = false });
                
                // Clear the entry field
                NewTaskTitle = string.Empty;
            }
    
            [RelayCommand]
            void DeleteTask(TodoItem item)
            {
                if (Tasks.Contains(item))
                {
                    Tasks.Remove(item);
                }
            }
        }
    }
                

    Why this is better:

    • ObservableObject: Implements INotifyPropertyChanged for you.
    • [ObservableProperty]: Generates a property named NewTaskTitle from the field newTaskTitle.
    • [RelayCommand]: Generates an ICommand called AddTaskCommand which the View can bind to.

    Building the View: XAML and Data Binding

    Now, let’s connect the ViewModel to the View. In .NET MAUI, we use the BindingContext to tell the XAML page which class is providing its data.

    
    <?xml version="1.0" encoding="utf-8" ?>
    <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 xmlns:viewmodel="clr-namespace:MauiMvvmGuide.ViewModels"
                 x:Class="MauiMvvmGuide.Views.MainPage"
                 x:DataType="viewmodel:MainViewModel">
    
        <VerticalStackLayout Padding="20" Spacing="15">
            
            <!-- Entry for new task title -->
            <Entry Text="{Binding NewTaskTitle}" 
                   Placeholder="Enter a new task..." />
    
            <!-- Button to trigger the AddTask command -->
            <Button Text="Add Task" 
                    Command="{Binding AddTaskCommand}" />
    
            <!-- List of tasks -->
            <CollectionView ItemsSource="{Binding Tasks}">
                <CollectionView.ItemTemplate>
                    <DataTemplate x:DataType="models:TodoItem">
                        <HorizontalStackLayout Spacing="10" Padding="5">
                            <CheckBox IsChecked="{Binding IsCompleted}" />
                            <Label Text="{Binding Title}" VerticalOptions="Center" />
                        </HorizontalStackLayout>
                    </DataTemplate>
                </CollectionView.ItemTemplate>
            </CollectionView>
    
        </VerticalStackLayout>
    </ContentPage>
                

    Key Concept: x:DataType

    Always use x:DataType. This is called Compiled Bindings. Without it, MAUI uses reflection at runtime to find properties, which is slow and error-prone. With it, the compiler checks that your bindings are correct, improving performance and catching bugs during development.

    Step-by-Step: Wiring Up Dependency Injection (DI)

    In modern .NET apps, we don’t usually create ViewModels manually with new MainViewModel(). Instead, we use the built-in Dependency Injection container. This allows us to inject services (like an API client) into our ViewModel easily.

    Step 1: Register in MauiProgram.cs

    
    public static class MauiProgram
    {
        public static MauiApp CreateMauiApp()
        {
            var builder = MauiApp.CreateBuilder();
            builder.UseMauiApp<App>();
    
            // Register Services
            builder.Services.AddSingleton<IDataService, MyDataService>();
    
            // Register ViewModels
            builder.Services.AddTransient<MainViewModel>();
    
            // Register Views
            builder.Services.AddTransient<MainPage>();
    
            return builder.Build();
        }
    }
                

    Step 2: Inject into the View’s Constructor

    Go to your MainPage.xaml.cs (the code-behind) and inject the ViewModel:

    
    public partial class MainPage : ContentPage
    {
        public MainPage(MainViewModel viewModel)
        {
            InitializeComponent();
            // Set the BindingContext to the injected ViewModel
            BindingContext = viewModel;
        }
    }
                

    Advanced MVVM: Navigation and Shell

    Navigating between pages in MVVM can be tricky because the ViewModel shouldn’t know about the UI “Page” objects. .NET MAUI Shell simplifies this using Route-based navigation.

    To navigate from a ViewModel:

    
    [RelayCommand]
    async Task GoToDetails(TodoItem item)
    {
        // Pass the object to the next page using Query Parameters
        var navigationParameter = new Dictionary<string, object>
        {
            { "Item", item }
        };
        
        await Shell.Current.GoToAsync("DetailsPage", navigationParameter);
    }
                

    On the receiving ViewModel, use the [QueryProperty] attribute to grab the data:

    
    [QueryProperty(nameof(Item), "Item")]
    public partial class DetailsViewModel : ObservableObject
    {
        [ObservableProperty]
        TodoItem item;
    }
                

    Common MVVM Mistakes and How to Fix Them

    1. Blocking the UI Thread

    The Mistake: Performing long-running tasks (like fetching API data) inside a property setter or a synchronous Command.

    The Fix: Always use async Task in your RelayCommands. The Community Toolkit supports [RelayCommand] async Task MyMethod() automatically.

    2. Forgetting to Use ObservableCollection

    The Mistake: Using List<T> for data bound to a CollectionView. When you add an item to a List, the UI doesn’t know it needs to refresh.

    The Fix: Use ObservableCollection<T>. It implements INotifyCollectionChanged, which tells the UI to add or remove rows dynamically.

    3. Massive ViewModels

    The Mistake: Putting 1,000 lines of logic into one ViewModel. This is just “Spaghetti Code 2.0.”

    The Fix: Use Services. If you are making an HTTP call, put that logic in a WeatherService class. Inject that service into the ViewModel. The ViewModel should only contain the logic necessary to support the View.

    4. Logic in the Code-Behind

    The Mistake: Handling button clicks in the .xaml.cs file via Clicked="OnButtonClicked".

    The Fix: Use Command="{Binding MyCommand}". This keeps your logic in the ViewModel, where it can be unit tested without a physical device or emulator.

    Performance Optimization for .NET MAUI MVVM

    Mobile devices have limited resources. To keep your app buttery smooth, follow these SEO-friendly performance tips:

    • One-Way Bindings: If a Label only displays data and never changes it, use Mode=OneWay or OneTime. This reduces the number of event listeners MAUI has to manage.
    • Image Loading: Don’t load massive high-res images into a list. The Model should provide optimized URIs or thumbnails.
    • Memory Leaks: Be careful with static events. If your ViewModel subscribes to a global event, unsubscribe in an “Unloaded” event to prevent memory leaks.
    • Compiled Bindings: I will repeat this: Use x:DataType everywhere! It significantly reduces CPU overhead during UI rendering.

    Real-World Example: A Weather Dashboard ViewModel

    Let’s look at a more complex ViewModel that handles loading states, error handling, and data transformation.

    
    public partial class WeatherViewModel : ObservableObject
    {
        private readonly IWeatherService _weatherService;
    
        [ObservableProperty]
        private string city;
    
        [ObservableProperty]
        private double temperature;
    
        [ObservableProperty]
        [NotifyPropertyChangedFor(nameof(IsNotLoading))]
        private bool isLoading;
    
        public bool IsNotLoading => !IsLoading;
    
        public WeatherViewModel(IWeatherService weatherService)
        {
            _weatherService = weatherService;
        }
    
        [RelayCommand]
        async Task RefreshWeatherAsync()
        {
            try 
            {
                IsLoading = true;
                var data = await _weatherService.GetWeatherForCity(City);
                Temperature = data.Temp;
            }
            catch (Exception ex)
            {
                // Handle error (e.g., show an alert)
                await Shell.Current.DisplayAlert("Error", "Could not fetch weather", "OK");
            }
            finally 
            {
                IsLoading = false;
            }
        }
    }
                

    This example demonstrates the [NotifyPropertyChangedFor] attribute, which is extremely useful for dependent properties like “IsNotLoading” that rely on “IsLoading”.

    Summary and Key Takeaways

    Mastering MVVM in .NET MAUI is the single most important skill for a cross-platform developer. It transforms a messy project into a professional, testable application.

    • Separation: Keep Models for data, Views for XAML, and ViewModels for logic.
    • Toolkit: Use the CommunityToolkit.Mvvm to eliminate boilerplate code via Source Generators.
    • Binding: Use x:DataType for compiled bindings to boost performance.
    • Commands: Replace event handlers with ICommand and RelayCommand.
    • DI: Register your ViewModels and Services in MauiProgram.cs.

    Frequently Asked Questions (FAQ)

    1. Do I HAVE to use MVVM for small apps?

    While you can use code-behind for a tiny “Hello World” app, it is better to practice MVVM even on small projects. It builds muscle memory and makes it much easier to expand the app later when “simple” becomes “complex.”

    2. What is the difference between ObservableCollection and List?

    A List<T> is just a collection of data in memory. An ObservableCollection<T> is a specialized collection that sends a “notification” to the UI every time an item is added, removed, or the entire list is cleared, allowing the UI to update automatically.

    3. How do I handle UI events like “Page Appearing” in MVVM?

    You can use EventToCommandBehavior from the .NET MAUI Community Toolkit. This allows you to map XAML events (like Appearing) directly to a Command in your ViewModel without writing code in the code-behind.

    4. Can I use MVVM with other frameworks like ReactiveUI?

    Yes, .NET MAUI is flexible. While the Community Toolkit is the most popular, you can use ReactiveUI or Prism if you prefer a different flavor of MVVM logic (like functional reactive programming).

    5. Why is my binding not working?

    The three most common reasons are: 1) You forgot to set the BindingContext. 2) You are binding to a field instead of a property (properties must have { get; set; }). 3) Your class doesn’t implement INotifyPropertyChanged.