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:
-
Allocating objects on the heap
-
Identifying objects that are no longer in use
-
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
-
Assemblies: Deployment units containing code, metadata, and resources
-
Namespaces: Logical organization to prevent naming conflicts
-
Memory Management: Stack (value types) vs Heap (reference types)
-
Garbage Collection: Automatic memory management with generational approach
-
Dispose Pattern: Proper cleanup of unmanaged resources
-
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!