A comprehensive guide to .NET Framework and C# development, covering language features, testing, build automation, and best practices from the early era of .NET development.
Table of Contents
- C# Language Fundamentals
- Collections and Generics
- ASP.NET Development
- Testing with NUnit and Rhino Mocks
- Build Automation with NAnt
- ADO.NET and Data Access
- Threading and Concurrency
- Component-Based Programming
- Development Tools
- Design Patterns in .NET
C# Language Fundamentals
The as Keyword for Safe Type Casting
The as keyword provides a safe way to perform type casting without throwing exceptions:
object o = new string('8', 1);
// Instead of direct casting with try/catch
try {
string s2 = (string)o;
Console.WriteLine("is string");
} catch(InvalidCastException) {
Console.WriteLine("is not string");
}
// Or checking with 'is' operator (requires double cast)
if( o is string ) {
string s2 = (string)o;
Console.WriteLine("is string");
}
// Preferred: use 'as' keyword
string s = o as string;
Console.WriteLine((null != s) ? "is String" : "is not string");
The as operator attempts to cast to the specified type and returns null if the cast fails, avoiding expensive exception handling.
String Manipulation in C#
C# provides powerful string manipulation capabilities:
// String interpolation and formatting
string name = "World";
string greeting = $"Hello, {name}!";
// String methods
string text = " Example Text ";
string trimmed = text.Trim();
string upper = text.ToUpper();
bool contains = text.Contains("Example");
// StringBuilder for efficient string building
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.Append($"Item {i} ");
}
string result = sb.ToString();
Working with Generics and Default Values
When working with generic types, use default(T) to get the default value:
public T GetDefaultValue<T>() {
return default(T);
}
// For reference types, returns null
// For value types, returns zero-equivalent
Virtual Members and Constructor Calls
Important: Calling virtual members from constructors can lead to unexpected behavior:
public class Base {
public Base() {
DoSomething(); // Dangerous - calls derived implementation
}
protected virtual void DoSomething() {
// Base implementation
}
}
public class Derived : Base {
private string _value = "initialized";
protected override void DoSomething() {
// This runs before _value is initialized!
Console.WriteLine(_value); // May print null/empty
}
}
Best Practice: Avoid calling virtual members in constructors.
Collections and Generics
Choosing the Right Collection Interface
Use the most abstract interface possible for maximum flexibility:
// Instead of specific types like List<T>
public void ProcessItems(List<string> items) { }
// Use interfaces for flexibility
public void ProcessItems(ICollection<string> items) { }
public void ProcessItems(IEnumerable<string> items) { } // Most flexible
// Interface hierarchy
public interface IList<T> : ICollection<T>, IEnumerable<T> {
int IndexOf(T item);
void Insert(int index, T item);
void RemoveAt(int index);
T this[int index] { get; set; }
}
public interface ICollection<T> : IEnumerable<T> {
void Add(T item);
void Clear();
bool Contains(T item);
void CopyTo(T[] array, int arrayIndex);
bool Remove(T item);
int Count { get; }
bool IsReadOnly { get; }
}
Power Collections Library
Consider using Wintellect’s Power Collections for enhanced functionality:
// Example: OrderedSet for sorted unique collections
var orderedSet = new OrderedSet<int>();
orderedSet.Add(3);
orderedSet.Add(1);
orderedSet.Add(2);
// Maintains sorted order: 1, 2, 3
ASP.NET Development
AJAX Integration
Creating AJAX call processors with ASP.NET:
public class AjaxCallProcessor : IHttpHandler {
public void ProcessRequest(HttpContext context) {
string action = context.Request["action"];
switch (action) {
case "getData":
ProcessGetData(context);
break;
default:
context.Response.StatusCode = 400;
break;
}
}
private void ProcessGetData(HttpContext context) {
// Process AJAX request
var data = GetDataFromDatabase();
context.Response.ContentType = "application/json";
context.Response.Write(SerializeToJson(data));
}
public bool IsReusable => false;
}
Avoiding Inline ASP.NET Code
Problem: Mixing server and client code in ASPX pages creates maintenance issues:
<!-- Avoid this -->
<script language="javascript">
var serverValue = '<%= GetServerValue() %>';
</script>
Solution: Use code-behind and proper separation:
// Code-behind
protected void Page_Load(object sender, EventArgs e) {
ClientScript.RegisterStartupScript(
this.GetType(),
"serverData",
$"var serverValue = '{GetServerValue()}';",
true);
}
Testing with NUnit and Rhino Mocks
ReSharper Test Template
Standard test class template for ReSharper:
using NUnit.Framework;
using Rhino.Mocks;
namespace $NAMESPACE$ {
[TestFixture]
public class $CLASS$ {
private MockRepository _mock;
[SetUp]
public void SetUp() {
_mock = new MockRepository();
}
[TearDown]
public void TearDown() {
_mock.VerifyAll();
}
[Test]
public void _Should() {
// Test implementation
}
}
}
Mocking with Rhino Mocks
[Test]
public void Should_Calculate_Correct_Total() {
// Arrange
MockRepository mockery = new MockRepository();
ICalculator calculator = mockery.Stub<ICalculator>();
IPriceService priceService = mockery.Stub<IPriceService>();
using (mockery.Record()) {
Expect.Call(priceService.GetPrice("item1")).Return(10.0m);
Expect.Call(calculator.Add(10.0m, 5.0m)).Return(15.0m);
}
using (mockery.Playback()) {
var service = new OrderService(calculator, priceService);
decimal total = service.CalculateTotal("item1", 5.0m);
Assert.AreEqual(15.0m, total);
}
}
NUnit vs MbUnit
NUnit Features:
- Mature, stable framework
- Simple attribute-based testing
- Good IDE integration
MbUnit Advantages:
- Row test attributes for parameterized tests
- Better assertion framework
- More flexible test organization
// MbUnit row test example
[RowTest]
[Row(1, 2, 3)]
[Row(2, 3, 5)]
[Row(-1, 1, 0)]
public void Should_Add_Numbers_Correctly(int a, int b, int expected) {
Assert.AreEqual(expected, Calculator.Add(a, b));
}
Build Automation with NAnt
Basic Project Structure
<project name="MyProject" default="build">
<property name="debug" value="true" />
<target name="init">
<mkdir dir="build" />
</target>
<target name="compile" depends="init">
<csc target="library" output="build\${project::get-name()}.dll" debug="${debug}">
<sources>
<include name="src\app\**\*.cs" />
<exclude name="src\app\**\AssemblyInfo.cs" />
</sources>
</csc>
</target>
<target name="build" depends="compile" />
</project>
Running Unit Tests
<target name="test.compile" depends="compile">
<csc target="library" output="build\${project::get-name()}.Test.dll" debug="${debug}">
<sources>
<include name="src\test\**\*.cs" />
<exclude name="src\test\**\AssemblyInfo.cs" />
</sources>
<references>
<include name="build\${project::get-name()}.dll" />
<include name="tools\nunit\bin\nunit.framework.dll" />
<include name="tools\rhinomocks\Rhino.Mocks.dll" />
</references>
</csc>
</target>
<target name="test" depends="test.compile">
<copy todir="build" flatten="true">
<fileset basedir="tools">
<include name="**\Rhino.Mocks.dll" />
</fileset>
</copy>
<copy todir="build" flatten="true">
<fileset basedir="tools\nunit\bin">
<include name="*.dll" />
</fileset>
</copy>
<exec basedir="tools\nunit\bin"
useruntimeengine="true"
workingdir="build"
program="nunit-console.exe"
commandline="${project::get-name()}.Test.dll /xml=${project::get-name()}.Test-Result.xml" />
</target>
Database Management
<target name="db.rebuild">
<sql connstring="server=localhost;database=mydb;integrated security=true"
transaction="true"
delimiter="GO">
<fileset>
<include name="database\drop_tables.sql" />
<include name="database\create_tables.sql" />
<include name="database\populate_data.sql" />
</fileset>
</sql>
</target>
Using C# 3.0 Compiler without Visual Studio 2008
<property name="framework.dir" value="C:\Windows\Microsoft.NET\Framework\v3.5" />
<target name="compile">
<csc target="exe"
output="MyApp.exe"
debug="true"
keyfile="MyKey.snk">
<sources>
<include name="**/*.cs" />
</sources>
<references>
<include name="${framework.dir}\System.Core.dll" />
</references>
</csc>
</target>
ADO.NET and Data Access
ADO.NET 2.0 Best Practices
// Use using statements for proper disposal
using (SqlConnection connection = new SqlConnection(connectionString)) {
connection.Open();
using (SqlCommand command = new SqlCommand(sql, connection)) {
command.Parameters.AddWithValue("@param1", value1);
using (SqlDataReader reader = command.ExecuteReader()) {
while (reader.Read()) {
// Process results
string value = reader.GetString("ColumnName");
}
}
}
}
// Use strongly-typed datasets when appropriate
DataSet dataSet = new DataSet();
SqlDataAdapter adapter = new SqlDataAdapter(command);
adapter.Fill(dataSet);
Connection String Management
// Store in configuration
string connectionString = ConfigurationManager
.ConnectionStrings["MyDatabase"]
.ConnectionString;
// Use connection pooling (enabled by default)
// Minimize connection lifetime
// Use parameterized queries to prevent SQL injection
Threading and Concurrency
Thread Priority Management
// Set thread priority appropriately
Thread.CurrentThread.Priority = ThreadPriority.BelowNormal;
// For background processing
Thread backgroundThread = new Thread(ProcessData) {
IsBackground = true,
Priority = ThreadPriority.BelowNormal
};
backgroundThread.Start();
Avoiding Deadlocks
// Always acquire locks in consistent order
private static readonly object lock1 = new object();
private static readonly object lock2 = new object();
public void Method1() {
lock (lock1) {
lock (lock2) {
// Work
}
}
}
public void Method2() {
lock (lock1) { // Same order as Method1
lock (lock2) {
// Work
}
}
}
Single Instance Application Pattern
internal static class Program {
[STAThread]
private static void Main() {
if (IsFirstInstance()) {
Application.ApplicationExit += OnApplicationExit;
Application.Run(new Form1());
}
}
private static bool IsFirstInstance() {
_mutex = new Mutex(false, Assembly.GetEntryAssembly().FullName);
bool owned = _mutex.WaitOne(TimeSpan.Zero, false);
return owned;
}
private static void OnApplicationExit(object sender, EventArgs e) {
_mutex.ReleaseMutex();
_mutex.Close();
}
private static Mutex _mutex;
}
Component-Based Programming
Interface-Based Design
// Define contracts through interfaces
public interface IDocumentProcessor {
void ProcessDocument(IDocument document);
bool CanProcess(string fileType);
}
public interface IDocument {
string Title { get; }
string Content { get; }
DateTime Created { get; }
}
// Implement specific processors
public class PdfProcessor : IDocumentProcessor {
public void ProcessDocument(IDocument document) {
// PDF-specific processing
}
public bool CanProcess(string fileType) {
return fileType.Equals(".pdf", StringComparison.OrdinalIgnoreCase);
}
}
Component Factory Pattern
public class ProcessorFactory {
private readonly List<IDocumentProcessor> _processors;
public ProcessorFactory() {
_processors = new List<IDocumentProcessor> {
new PdfProcessor(),
new WordProcessor(),
new TextProcessor()
};
}
public IDocumentProcessor GetProcessor(string fileType) {
return _processors.FirstOrDefault(p => p.CanProcess(fileType));
}
}
Disposable Pattern
public class ResourceManager : IDisposable {
private bool _disposed = false;
private FileStream _fileStream;
public void DoWork() {
CheckDisposed();
// Use resources
}
public void Dispose() {
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing) {
if (!_disposed) {
if (disposing) {
// Dispose managed resources
_fileStream?.Dispose();
}
// Dispose unmanaged resources
_disposed = true;
}
}
private void CheckDisposed() {
if (_disposed) {
throw new ObjectDisposedException(GetType().Name);
}
}
}
Development Tools
ReSharper Productivity
ReSharper enhances C# development with:
- Code generation: Properties, constructors, overrides
- Refactoring: Rename, extract method, move to namespace
- Navigation: Go to declaration, find usages
- Code analysis: Suggestions and warnings
- Live templates: Reusable code snippets
Visual Studio Tips
// Use regions for code organization
#region Private Fields
private string _name;
private int _age;
#endregion
// XML documentation
/// <summary>
/// Calculates the total price including tax
/// </summary>
/// <param name="basePrice">The base price before tax</param>
/// <param name="taxRate">The tax rate (e.g., 0.08 for 8%)</param>
/// <returns>The total price including tax</returns>
public decimal CalculateTotal(decimal basePrice, decimal taxRate) {
return basePrice * (1 + taxRate);
}
Windows Live Writer for Documentation
Windows Live Writer provides a rich text editor for creating documentation and blog posts with:
- WYSIWYG editing
- Plugin support
- Code syntax highlighting
- Image handling
Design Patterns in .NET
Event Aggregator Pattern
public interface IEventAggregator {
void Subscribe<T>(Action<T> handler);
void Publish<T>(T eventObj);
}
public class EventAggregator : IEventAggregator {
private readonly Dictionary<Type, List<Delegate>> _handlers
= new Dictionary<Type, List<Delegate>>();
public void Subscribe<T>(Action<T> handler) {
var eventType = typeof(T);
if (!_handlers.ContainsKey(eventType)) {
_handlers[eventType] = new List<Delegate>();
}
_handlers[eventType].Add(handler);
}
public void Publish<T>(T eventObj) {
var eventType = typeof(T);
if (_handlers.ContainsKey(eventType)) {
foreach (Action<T> handler in _handlers[eventType]) {
handler(eventObj);
}
}
}
}
Identity Map Pattern
public class IdentityMap<TKey, TValue> {
private readonly Dictionary<TKey, TValue> _map
= new Dictionary<TKey, TValue>();
public void Add(TKey key, TValue value) {
_map[key] = value;
}
public TValue Get(TKey key) {
return _map.ContainsKey(key) ? _map[key] : default(TValue);
}
public bool Contains(TKey key) {
return _map.ContainsKey(key);
}
}
// Usage in data access layer
public class CustomerRepository {
private readonly IdentityMap<int, Customer> _customerMap
= new IdentityMap<int, Customer>();
public Customer GetById(int id) {
if (_customerMap.Contains(id)) {
return _customerMap.Get(id);
}
var customer = LoadFromDatabase(id);
_customerMap.Add(id, customer);
return customer;
}
}
Adapter Pattern
// Legacy interface
public interface ILegacyPaymentProcessor {
void ProcessPayment(string cardNumber, double amount);
}
// New interface
public interface IModernPaymentProcessor {
PaymentResult ProcessPayment(PaymentInfo payment);
}
// Adapter
public class PaymentAdapter : IModernPaymentProcessor {
private readonly ILegacyPaymentProcessor _legacyProcessor;
public PaymentAdapter(ILegacyPaymentProcessor legacyProcessor) {
_legacyProcessor = legacyProcessor;
}
public PaymentResult ProcessPayment(PaymentInfo payment) {
try {
_legacyProcessor.ProcessPayment(payment.CardNumber, payment.Amount);
return new PaymentResult { Success = true };
} catch (Exception ex) {
return new PaymentResult {
Success = false,
ErrorMessage = ex.Message
};
}
}
}
Best Practices Summary
Exception Handling
- Don’t swallow exceptions
- Use specific exception types
- Log exceptions with context
- Fail fast when appropriate
Performance
- Use
StringBuilderfor string concatenation - Prefer
asover direct casting - Choose appropriate collection interfaces
- Dispose resources properly
Code Quality
- Follow SOLID principles
- Write unit tests
- Use meaningful names
- Keep methods small and focused
Architecture
- Separate concerns
- Program to interfaces
- Use dependency injection
- Implement proper error handling
This guide represents foundational .NET and C# development practices from the framework’s early years, many of which remain relevant today. While newer versions of .NET have introduced additional features and improvements, these core concepts provide a solid foundation for understanding enterprise .NET development.
This guide consolidates knowledge from multiple blog posts written between 2006-2008, covering practical .NET development experience during the framework’s early adoption period.