Menu

C# & ASP.NET Topic

.NET Runtime Basics

Assemblies, namespaces, memory management, garbage collector fundamentals.

1. .NET Architecture Overview

The Big Picture:

text

Your C# Application
         ↓
Common Language Infrastructure (CLI)
         ↓
Common Language Runtime (CLR) ←→ Base Class Library (BCL)
         ↓
Operating System (Windows, Linux, macOS)

2. Assemblies

What are Assemblies?

Assemblies are the fundamental units of deployment and versioning in .NET. They contain compiled code, metadata, and resources.

Assembly Structure:

text

Assembly (.dll or .exe)
├── Assembly Manifest (Metadata)
├── Type Metadata
├── MSIL Code (Intermediate Language)
└── Resources (optional)

Detailed Examples:

Creating and Examining Assemblies:

csharp

using System;
using System.Reflection;

// Simple class library assembly
public class Calculator
{
    public int Add(int a, int b) => a + b;
    public int Subtract(int a, int b) => a - b;
}

// Program to examine assembly information
public class AssemblyInspector
{
    public static void ExamineAssembly()
    {
        // Get the current executing assembly
        Assembly currentAssembly = Assembly.GetExecutingAssembly();
        
        Console.WriteLine($"Assembly Name: {currentAssembly.GetName().Name}");
        Console.WriteLine($"Version: {currentAssembly.GetName().Version}");
        Console.WriteLine($"Location: {currentAssembly.Location}");
        Console.WriteLine($"Full Name: {currentAssembly.FullName}");
        
        // Get all types in the assembly
        Console.WriteLine("\nTypes in this assembly:");
        foreach (Type type in currentAssembly.GetTypes())
        {
            Console.WriteLine($"  - {type.FullName}");
            
            // Get methods for each type
            foreach (MethodInfo method in type.GetMethods(
                BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
            {
                Console.WriteLine($"    Method: {method.Name}");
            }
        }
    }
}

// Usage
AssemblyInspector.ExamineAssembly();

Working with Multiple Assemblies:

csharp

using System;
using System.Reflection;

public class MultiAssemblyDemo
{
    public static void LoadAndInspectAssembly()
    {
        // Load assembly by path
        try
        {
            Assembly mathAssembly = Assembly.LoadFrom("MathLibrary.dll");
            
            // Get assembly attributes
            var companyAttr = mathAssembly.GetCustomAttribute<AssemblyCompanyAttribute>();
            Console.WriteLine($"Company: {companyAttr?.Company}");
            
            // Create instance from loaded assembly
            Type calculatorType = mathAssembly.GetType("MathLibrary.Calculator");
            object calculatorInstance = Activator.CreateInstance(calculatorType);
            
            // Invoke method dynamically
            MethodInfo addMethod = calculatorType.GetMethod("Add");
            object result = addMethod.Invoke(calculatorInstance, new object[] { 5, 3 });
            Console.WriteLine($"5 + 3 = {result}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error loading assembly: {ex.Message}");
        }
    }
    
    public static void StrongNamedAssemblies()
    {
        // Strong-named assemblies have versioning and digital signatures
        AssemblyName assemblyName = new AssemblyName("MyStrongNamedAssembly");
        assemblyName.Version = new Version(1, 0, 0, 0);
        assemblyName.CultureInfo = System.Globalization.CultureInfo.InvariantCulture;
        
        // In practice, strong naming is done via signing with a key file
        // sn -k MyKeyPair.snk  (in Visual Studio Developer Command Prompt)
    }
}

Assembly Manifest Example:

csharp

// AssemblyInfo.cs (typically in Properties folder)
[assembly: AssemblyTitle("MyBusinessApp")]
[assembly: AssemblyDescription("A sample business application")]
[assembly: AssemblyCompany("My Company Inc.")]
[assembly: AssemblyProduct("MyBusinessApp")]
[assembly: AssemblyCopyright("Copyright © 2023")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
// [assembly: AssemblyKeyFile("MyKeyPair.snk")] // For strong naming

// The manifest contains:
// - Assembly identity (name, version, culture, public key)
// - List of files in the assembly
// - Referenced assemblies
// - Security permissions
// - Custom attributes

3. Namespaces

What are Namespaces?

Namespaces provide a logical grouping of related types and prevent naming conflicts.

Detailed Examples:

Basic Namespace Organizati

on:

csharp

// Business Layer
namespace MyCompany.Business.Models
{
    public class Customer
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
    }
}

namespace MyCompany.Business.Services
{
    public interface ICustomerService
    {
        void AddCustomer(Customer customer);
        Customer GetCustomer(int id);
    }
    
    public class CustomerService : ICustomerService
    {
        private readonly List<Customer> _customers = new List<Customer>();
        
        public void AddCustomer(Customer customer)
        {
            _customers.Add(customer);
        }
        
        public Customer GetCustomer(int id)
        {
            return _customers.FirstOrDefault(c => c.Id == id);
        }
    }
}

// Data Access Layer
namespace MyCompany.Data.Repositories
{
    public interface IRepository<T>
    {
        T GetById(int id);
        void Save(T entity);
    }
    
    public class CustomerRepository : IRepository<Customer>
    {
        public Customer GetById(int id)
        {
            // Database access logic
            return new Customer { Id = id, Name = "John Doe", Email = "john@example.com" };
        }
        
        public void Save(Customer entity)
        {
            // Save to database
        }
    }
}

Using Namespaces:

csharp

// Different ways to use namespaces
using System;
using MyCompany.Business.Models;
using MyCompany.Business.Services;
using CustomerService = MyCompany.Business.Services.CustomerService; // Alias

namespace MyCompany.Application
{
    public class Program
    {
        public static void Main()
        {
            // Fully qualified name (no using needed)
            var customer = new MyCompany.Business.Models.Customer();
            
            // With using directive
            var service = new CustomerService();
            
            // Using alias
            var service2 = new CustomerService();
        }
    }
}

// Global using (C# 10+)
// GlobalUsings.cs
global using System;
global using System.Collections.Generic;
global using MyCompany.Business.Models;

Namespace Best Practices:

csharp

// Good namespace organization
namespace CompanyName.ProductName.ModuleName.FeatureName
{
    // Example:
    // Microsoft.AspNetCore.Mvc.Filters
    // MyCompany.ECommerce.Inventory.Services
    // MyCompany.ECommerce.Shipping.Repositories
}

namespace MyCompany.ECommerce.Domain
{
    // Domain models and business logic
    public class Product { }
    public class Order { }
    public interface IOrderValidator { }
}

namespace MyCompany.ECommerce.Application
{
    // Application services
    public class OrderProcessingService { }
}

namespace MyCompany.ECommerce.Infrastructure
{
    // External concerns: databases, APIs, etc.
    public class SqlOrderRepository { }
}

4. Memory Management

Stack vs Heap Memory:

Stack Memory:

  • Stores value types and method call information

  • Fast allocation/deallocation (LIFO)

  • Limited size

  • Automatic cleanup when method exits

Heap Memory:

  • Stores reference type objects

  • Slower allocation (requires GC)

  • Larger size

  • Managed by Garbage Collector

Detailed Examples:

Value Types vs Reference Types:

csharp

public class MemoryDemo
{
    // Value types (stored on stack when local, or inside objects on heap)
    public struct Point  // struct is a value type
    {
        public int X;
        public int Y;
        
        public Point(int x, int y)
        {
            X = x;
            Y = y;
        }
    }
    
    // Reference type (always stored on heap)
    public class Rectangle  // class is a reference type
    {
        public Point TopLeft;  // Value type embedded in reference type
        public int Width;
        public int Height;
    }
    
    public static void DemonstrateMemoryAllocation()
    {
        // Value types on stack
        int number = 42;                    // Stack
        Point point = new Point(10, 20);    // Stack (if local variable)
        
        // Reference types on heap
        Rectangle rect = new Rectangle();   // Object on heap, reference on stack
        rect.TopLeft = point;
        rect.Width = 100;
        rect.Height = 50;
        
        // Arrays are reference types
        int[] numbers = new int[1000];      // Array object on heap
        Point[] points = new Point[100];    // Array on heap, Points stored inline in array
        
        // String is a reference type (special behavior)
        string name = "John";               // String object on heap
        string greeting = name + " Doe";    // New string object on heap
    }
    
    public static void MethodStackDemo()
    {
        int localVar = 100;           // On stack
        Point localPoint = new Point(5, 5);  // On stack
        
        // When method is called, stack frame is created with:
        // - Return address
        // - Parameters
        // - Local variables
        // When method exits, stack frame is destroyed
    }
}

Memory Allocation Patterns:

csharp

public class MemoryPatterns
{
    public static void DemonstrateAllocationPatterns()
    {
        // 1. Temporary objects (short-lived)
        string temp = DateTime.Now.ToString();  // Short-lived
        
        // 2. Long-lived objects
        var cache = new Dictionary<string, object>();  // Long-lived
        
        // 3. Large objects (>85KB go to Large Object Heap)
        byte[] largeBuffer = new byte[100000];  // Goes to LOH
        
        // 4. Object graphs
        var parent = new TreeNode("Root");
        parent.Children.Add(new TreeNode("Child1"));  // Object graph
        parent.Children.Add(new TreeNode("Child2"));
    }
}

public class TreeNode
{
    public string Value { get; set; }
    public List<TreeNode> Children { get; set; }
    
    public TreeNode(string value)
    {
        Value = value;
        Children = new List<TreeNode>();
    }
}
5. Garbage Collector Fundamentals

What is the Garbage Collector?

The GC automatically manages memory by:

  1. Allocating objects on the heap

  2. Identifying objects that are no longer in use

  3. Reclaiming memory used by unused objects

Generational Garbage Collection:

text

Generation 0: Young objects (short-lived)
Generation 1: Middle-aged objects
Generation 2: Old objects (long-lived)
Large Object Heap (LOH): Objects >85KB

Detailed Examples:

GC Generations in Action:

csharp

public class GCDemo
{
    public static void DemonstrateGenerations()
    {
        // Create some objects
        object gen0Object = new object();  // Starts in Gen 0
        
        Console.WriteLine($"Initial generation: {GC.GetGeneration(gen0Object)}"); // Gen 0
        
        // Force a Gen 0 collection
        GC.Collect(0);
        Console.WriteLine($"After Gen 0 collection: {GC.GetGeneration(gen0Object)}"); // May survive to Gen 1
        
        // Create more objects
        for (int i = 0; i < 10000; i++)
        {
            var temp = new object();
        }
        
        // Force another collection
        GC.Collect();
        Console.WriteLine($"After full collection: {GC.GetGeneration(gen0Object)}"); // May be Gen 1 or 2
        
        // Get GC memory info
        DisplayGCInfo();
    }
    
    public static void DisplayGCInfo()
    {
        Console.WriteLine($"Total Memory: {GC.GetTotalMemory(false):N0} bytes");
        Console.WriteLine($"Max Generation: {GC.MaxGeneration}");
        
        for (int i = 0; i <= GC.MaxGeneration; i++)
        {
            Console.WriteLine($"Gen {i} collection count: {GC.CollectionCount(i)}");
        }
    }
    
    public static void MemoryPressureDemo()
    {
        // Simulate memory pressure
        List<byte[]> buffers = new List<byte[]>();
        
        try
        {
            for (int i = 0; i < 100; i++)
            {
                // Allocate 1MB chunks
                byte[] buffer = new byte[1024 * 1024]; // 1MB
                buffers.Add(buffer);
                
                if (i % 10 == 0)
                {
                    Console.WriteLine($"Allocated {(i + 1)}MB, Total Memory: {GC.GetTotalMemory(false) / 1024 / 1024}MB");
                }
            }
        }
        catch (OutOfMemoryException)
        {
            Console.WriteLine("Out of memory! Forcing collection...");
            GC.Collect();
        }
    }
}

Finalization and Dispose Pattern:

csharp

public class ResourceHolder : IDisposable
{
    private IntPtr nativeResource;  // Unmanaged resource
    private List<byte[]> managedResource;  // Managed resource
    private bool disposed = false;
    
    public ResourceHolder()
    {
        nativeResource = Marshal.AllocHGlobal(1000);  // Unmanaged memory
        managedResource = new List<byte[]>();
        
        Console.WriteLine("ResourceHolder created");
    }
    
    // Finalizer (destructor) - backup cleanup
    ~ResourceHolder()
    {
        Console.WriteLine("Finalizer called");
        Dispose(false);
    }
    
    // IDisposable implementation
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);  // No need for finalization
    }
    
    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // Clean up managed resources
                managedResource?.Clear();
                managedResource = null;
                Console.WriteLine("Managed resources cleaned up");
            }
            
            // Clean up unmanaged resources
            if (nativeResource != IntPtr.Zero)
            {
                Marshal.FreeHGlobal(nativeResource);
                nativeResource = IntPtr.Zero;
                Console.WriteLine("Unmanaged resources cleaned up");
            }
            
            disposed = true;
        }
    }
    
    public void UseResource()
    {
        if (disposed)
            throw new ObjectDisposedException(nameof(ResourceHolder));
            
        Console.WriteLine("Using resource...");
    }
}

// Usage of Dispose pattern
public class DisposePatternDemo
{
    public static void DemonstrateDispose()
    {
        // Using statement ensures Dispose is called
        using (var resource = new ResourceHolder())
        {
            resource.UseResource();
        } // Dispose() called automatically here
        
        Console.WriteLine("Resource disposed");
        
        // Without using statement
        ResourceHolder resource2 = null;
        try
        {
            resource2 = new ResourceHolder();
            resource2.UseResource();
        }
        finally
        {
            resource2?.Dispose();
        }
    }
    
    public static void FinalizationDemo()
    {
        // Object with finalizer
        var resource = new ResourceHolder();
        resource.UseResource();
        
        // Don't call Dispose - finalizer will eventually run
        resource = null;
        
        // Force GC to demonstrate finalizer
        GC.Collect();
        GC.WaitForPendingFinalizers();  // Wait for finalizers to complete
    }
}

Weak References for Memory Optimization:

csharp

public class WeakReferenceDemo
{
    public static void DemonstrateWeakReferences()
    {
        // Strong reference - prevents GC
        var strongData = new byte[1024 * 1024]; // 1MB
        
        // Weak reference - doesn't prevent GC
        var weakRef = new WeakReference(strongData);
        
        Console.WriteLine($"Weak reference is alive: {weakRef.IsAlive}");
        
        // Access the object through weak reference
        if (weakRef.Target is byte[] data)
        {
            Console.WriteLine($"Data length: {data.Length}");
        }
        
        // Remove strong reference
        strongData = null;
        
        // Force GC
        GC.Collect();
        GC.WaitForPendingFinalizers();
        
        Console.WriteLine($"After GC - Weak reference is alive: {weakRef.IsAlive}");
        
        // Weak references are useful for:
        // - Caching large objects
        // - Preventing memory leaks in event handlers
        // - Managing expensive resources that can be recreated
    }
    
    public static void WeakReferenceCache()
    {
        var cache = new Dictionary<string, WeakReference>();
        
        // Add to cache
        var largeObject = new byte[1024 * 512]; // 512KB
        cache["data1"] = new WeakReference(largeObject);
        
        // Later, try to retrieve
        if (cache.TryGetValue("data1", out WeakReference weakRef) && weakRef.IsAlive)
        {
            var data = weakRef.Target as byte[];
            Console.WriteLine("Retrieved from cache");
        }
        else
        {
            Console.WriteLine("Object was collected, recreate it");
            // Recreate the object...
        }
    }
}

GC Settings and Best Practices:

csharp

public class GCBestPractices
{
    public static void DemonstrateBestPractices()
    {
        // 1. Avoid unnecessary allocations
        string result = "";
        
        //  BAD - creates multiple string objects
        for (int i = 0; i < 100; i++)
        {
            result += i.ToString();  // Creates new string each time
        }
        
        //  GOOD - uses StringBuilder
        var sb = new StringBuilder();
        for (int i = 0; i < 100; i++)
        {
            sb.Append(i);
        }
        result = sb.ToString();
        
        // 2. Use object pooling for frequently created/destroyed objects
        var pool = new ObjectPool<ExpensiveObject>(() => new ExpensiveObject());
        using (var rented = pool.Rent())
        {
            rented.Object.DoSomething();
        } // Returns to pool instead of being GC'd
        
        // 3. Be careful with event handlers to prevent memory leaks
        var publisher = new EventPublisher();
        var subscriber = new EventSubscriber();
        
        publisher.SomeEvent += subscriber.HandleEvent;  // Strong reference
        
        // To avoid leak, unsubscribe when done:
        // publisher.SomeEvent -= subscriber.HandleEvent;
        
        // 4. Use structs for small, short-lived data
        Point[] points = new Point[1000];  // Better than class Point[] for performance
    }
}

// Simple object pool implementation
public class ObjectPool<T> where T : class
{
    private readonly ConcurrentBag<T> _objects;
    private readonly Func<T> _objectGenerator;
    
    public ObjectPool(Func<T> objectGenerator)
    {
        _objects = new ConcurrentBag<T>();
        _objectGenerator = objectGenerator ?? throw new ArgumentNullException(nameof(objectGenerator));
    }
    
    public PooledObject<T> Rent()
    {
        if (!_objects.TryTake(out T item))
        {
            item = _objectGenerator();
        }
        return new PooledObject<T>(item, this);
    }
    
    public void Return(T item)
    {
        _objects.Add(item);
    }
}

public struct PooledObject<T> : IDisposable where T : class
{
    private readonly ObjectPool<T> _pool;
    
    public T Object { get; }
    
    public PooledObject(T obj, ObjectPool<T> pool)
    {
        Object = obj;
        _pool = pool;
    }
    
    public void Dispose()
    {
        _pool.Return(Object);
    }
}

public class ExpensiveObject
{
    private readonly byte[] _data = new byte[1024 * 1024]; // 1MB
    
    public void DoSomething()
    {
        // Simulate work
        Thread.Sleep(10);
    }
}

// Supporting classes for examples
public class EventPublisher
{
    public event EventHandler SomeEvent;
    
    public void RaiseEvent()
    {
        SomeEvent?.Invoke(this, EventArgs.Empty);
    }
}

public class EventSubscriber
{
    public void HandleEvent(object sender, EventArgs e)
    {
        Console.WriteLine("Event handled");
    }
}

6. Real-World Complete Example

csharp

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;

namespace RuntimeBasicsDemo
{
    public class ApplicationMemoryMonitor : IDisposable
    {
        private readonly Timer _monitorTimer;
        private bool _disposed = false;
        
        public ApplicationMemoryMonitor()
        {
            // Monitor memory every 5 seconds
            _monitorTimer = new Timer(MonitorMemory, null, 0, 5000);
            
            Console.WriteLine("Memory monitor started");
        }
        
        private void MonitorMemory(object state)
        {
            var process = Process.GetCurrentProcess();
            
            Console.WriteLine($"\n--- Memory Stats ---");
            Console.WriteLine($"Process Memory: {process.WorkingSet64 / 1024 / 1024}MB");
            Console.WriteLine($"GC Total Memory: {GC.GetTotalMemory(false) / 1024 / 1024}MB");
            Console.WriteLine($"GC Gen 0 Collections: {GC.CollectionCount(0)}");
            Console.WriteLine($"GC Gen 1 Collections: {GC.CollectionCount(1)}");
            Console.WriteLine($"GC Gen 2 Collections: {GC.CollectionCount(2)}");
        }
        
        public void SimulateWork()
        {
            var dataProcessor = new DataProcessor();
            
            for (int i = 0; i < 100; i++)
            {
                using (var batch = dataProcessor.CreateBatch($"Batch_{i}"))
                {
                    batch.ProcessData(i);
                }
                
                if (i % 10 == 0)
                {
                    // Force GC occasionally to see behavior
                    GC.Collect(0);
                }
                
                Thread.Sleep(100);
            }
        }
        
        public void Dispose()
        {
            if (!_disposed)
            {
                _monitorTimer?.Dispose();
                _disposed = true;
                Console.WriteLine("Memory monitor disposed");
            }
        }
    }
    
    public class DataProcessor
    {
        public DataBatch CreateBatch(string name)
        {
            return new DataBatch(name);
        }
    }
    
    public class DataBatch : IDisposable
    {
        private readonly string _name;
        private readonly List<byte[]> _dataChunks;
        private bool _disposed = false;
        
        public DataBatch(string name)
        {
            _name = name;
            _dataChunks = new List<byte[]>();
            Console.WriteLine($"Created batch: {name}");
        }
        
        public void ProcessData(int index)
        {
            // Simulate data processing with memory allocation
            byte[] chunk = new byte[1024 * 50]; // 50KB chunk
            _dataChunks.Add(chunk);
            
            // Simulate work
            Thread.Sleep(50);
        }
        
        public void Dispose()
        {
            if (!_disposed)
            {
                _dataChunks.Clear();
                Console.WriteLine($"Disposed batch: {_name}");
                _disposed = true;
            }
        }
    }
    
    class Program
    {
        static void Main()
        {
            Console.WriteLine("=== .NET Runtime Basics Demo ===");
            
            // Show initial assembly info
            var assembly = typeof(Program).Assembly;
            Console.WriteLine($"Running: {assembly.GetName().Name} v{assembly.GetName().Version}");
            
            using (var monitor = new ApplicationMemoryMonitor())
            {
                // Simulate application work
                monitor.SimulateWork();
                
                Console.WriteLine("\nWork completed. Press any key to exit...");
                Console.ReadKey();
            }
            
            // Final GC stats
            Console.WriteLine("\n=== Final GC Statistics ===");
            for (int i = 0; i <= GC.MaxGeneration; i++)
            {
                Console.WriteLine($"Gen {i} collections: {GC.CollectionCount(i)}");
            }
        }
    }
}

Key .NET Runtime Concepts Summary

  1. Assemblies: Deployment units containing code, metadata, and resources

  2. Namespaces: Logical organization to prevent naming conflicts

  3. Memory Management: Stack (value types) vs Heap (reference types)

  4. Garbage Collection: Automatic memory management with generational approach

  5. Dispose Pattern: Proper cleanup of unmanaged resources

  6. Performance: Understanding allocation patterns and GC behavior

These fundamentals are crucial for building efficient, reliable .NET applications that make the best use of the runtime's capabilities.

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?