Unit 3: Advanced C# Programming

CSE253 — .Net Programming 9 min read

I. Orientation

Advanced C# programming combines expressive language features with design principles for building reusable, maintainable, and responsive .NET applications. LINQ simplifies data querying, delegates and events support flexible communication, interfaces and generics promote abstraction and reuse, and asynchronous programming keeps applications responsive. The SOLID principles provide a design framework for controlling dependency and change.

  • Type safety: The compiler checks types such as int, string, interfaces, and generic parameters before execution.
  • Abstraction: Interfaces and delegates expose required behavior without forcing callers to know implementation details.
  • Deferred execution: Many LINQ queries execute only when their results are enumerated.
  • Loose coupling: Dependencies should be represented by abstractions, allowing implementations to change independently.
  • Resource awareness: Asynchronous code should avoid blocking threads, especially during I/O operations.
  • Contract consistency: An implementation must honor the behavior promised by its interface or base abstraction.

II. Language Integrated Query (LINQ)

A. LINQ

LINQ is a C# query technology that applies a consistent syntax to collections, databases, XML, and other data sources.

  • Query expression: A query can filter and project objects using familiar clauses.
CSHARP
  var names = from student in students
              where student.Mark >= 50
              select student.Name;

Here, students is the source sequence, Mark >= 50 is the filter, and Name is the projected result.

  • Method syntax: The same operation can use extension methods and lambda expressions.
CSHARP
  var names = students
      .Where(s => s.Mark >= 50)
      .Select(s => s.Name);
  • Deferred execution: Where and Select usually store the query definition; iteration with foreach, ToList(), or Count() triggers execution.
  • Immediate execution: ToList(), ToArray(), and ToDictionary() materialize results immediately, which is useful when the source may change.
  • Composition: Queries can be built step by step, for example filtering with Where, ordering with OrderBy, grouping with GroupBy, and aggregating with Sum or Average.
  • Provider distinction: IEnumerable<T> queries normally run in memory, whereas IQueryable<T> can translate expressions into another language, such as SQL.

B. LINQ to Collections

LINQ to Collections queries in-memory objects implementing IEnumerable<T>, including arrays, lists, and sets.

  • Filtering: Where returns elements satisfying a predicate.
CSHARP
  var adults = people.Where(p => p.Age >= 18).ToList();

The predicate p => p.Age >= 18 is evaluated for each Person.

  • Projection: Select transforms each source element, such as converting Person objects into names.
  • Ordering: OrderBy(p => p.Name) sorts ascending; OrderByDescending(p => p.Mark) sorts descending.
  • Element selection: FirstOrDefault() returns the first match or the default value, while Single() requires exactly one match and throws if zero or multiple matches exist.
  • Aggregation: Count(), Min(), Max(), Sum(), and Average() calculate summary values; Any() efficiently checks whether at least one element exists.
  • Performance limitation: Repeated enumeration can repeat work. Materializing with ToList() is appropriate when a result will be reused, but it allocates additional memory.

III. Delegates and Events

Delegates and events provide type-safe mechanisms for treating methods as values and notifying objects when something occurs.

A. Delegates and Events

A delegate is a type-safe reference to a method; an event restricts delegate invocation so that only its declaring class can raise the notification.

  • Delegate declaration: The signature specifies the return type and parameters.
CSHARP
  public delegate void MessageHandler(string message);

MessageHandler can reference any compatible method returning void and accepting one string.

  • Invocation: A delegate may point to a method or lambda.
CSHARP
  MessageHandler handler = message => Console.WriteLine(message);
  handler("Saved");
  • Built-in delegates: Action<T> returns void, Func<T, TResult> returns a value, and Predicate<T> returns bool.
  • Multicast behavior: Delegates can contain multiple methods using +=; invocation calls them in registration order.
  • Event contract: An event commonly uses EventHandler or EventHandler<TEventArgs>.
CSHARP
  public event EventHandler? Completed;

  protected virtual void OnCompleted() =>
      Completed?.Invoke(this, EventArgs.Empty);
  • Encapsulation: External code can subscribe with += and unsubscribe with -=, but cannot directly raise the event.
  • Lifetime concern: Long-lived publishers retaining subscribers can cause memory retention; unsubscribe when the subscriber no longer needs notifications.

IV. Interfaces

Interfaces define contracts that classes or structs implement, enabling polymorphism and dependency substitution.

A. Interfaces

An interface states what an object can do without prescribing how it does it.

  • Contract: IPrintable might declare void Print(), requiring every implementation to provide that operation.
  • Multiple implementation: A class can implement multiple interfaces, such as class Report : IPrintable, IExportable.
  • Polymorphism: Code can depend on IEnumerable<string> rather than List<string>, allowing arrays, lists, or custom sequences.
  • Explicit implementation: A class can provide different interface members with the same name, but the member is accessed through the interface reference.
  • Default members: Modern C# interfaces may contain default implementations, but interfaces should remain focused contracts rather than large utility classes.
  • Testing benefit: A service accepting IEmailSender can receive a fake sender during testing instead of contacting a real mail server.

V. Generics in C

Generics define classes, methods, and interfaces with type parameters, providing reuse while preserving compile-time type safety.

A. Generics in C

Generics allow one implementation to work with many types without unsafe casts or unnecessary boxing.

  • Generic type: List<int> stores integers, while List<string> stores strings; the compiler rejects an incorrect insertion.
  • Generic method:
CSHARP
  static T First<T>(IEnumerable<T> items) => items.First();

T is a type parameter inferred from the supplied sequence.

  • Constraints: Constraints limit valid type arguments.
CSHARP
  static T Create<T>() where T : new() => new T();

The new() constraint requires a public parameterless constructor.

  • Common constraints: where T : class requires a reference type; struct requires a non-nullable value type; an interface constraint requires implementation of that interface.
  • Value-type efficiency: List<int> stores integers without converting each value to object, avoiding many boxing operations.
  • Generic abstractions: IRepository<T> can describe storage operations for Customer, Order, or another entity while retaining the entity type.
  • Variance: IEnumerable<Derived> can be used where IEnumerable<Base> is expected because IEnumerable<out T> is covariant.

VI. Asynchronous Programming using Async and Await

Asynchronous programming allows an operation to pause without blocking the current thread while it waits for I/O or another asynchronous result.

A. Asynchronous Programming using Async and Await

The async and await keywords express continuation-based code in a readable form.

  • Task result: Task represents an operation with no returned value; Task<T> represents an operation producing a T.
CSHARP
  public async Task<string> LoadAsync(HttpClient client, string url)
  {
      return await client.GetStringAsync(url);
  }

The method returns a Task<string> and resumes after the HTTP operation completes.

  • Non-blocking wait: await does not normally block the calling thread; .Result and .Wait() can block and may cause deadlocks in context-sensitive applications.
  • Concurrency: Independent operations can be started before awaiting them.
CSHARP
  Task<string> first = LoadAsync(client, url1);
  Task<string> second = LoadAsync(client, url2);
  string[] pages = await Task.WhenAll(first, second);
  • Exceptions: Exceptions from an awaited task are observed at the await expression and can be handled with try/catch.
  • Cancellation: Pass a CancellationToken to operations and call ThrowIfCancellationRequested() where appropriate.
  • CPU versus I/O: Async is especially useful for file, database, and network I/O. CPU-heavy work may require Task.Run, used carefully because it consumes thread-pool resources.
  • Naming convention: Asynchronous methods conventionally end in Async, such as SaveAsync, and should propagate asynchronous calls instead of synchronously wrapping them.

VII. Single Responsibility Principle (SRP)

SRP states that a class should have one responsibility and therefore one reason to change.

A. Single Responsibility Principle (SRP)

SRP separates unrelated causes of modification so that changes remain localized.

  • Mixed responsibility: A Report class that formats content, writes files, and emails users changes for presentation, storage, and delivery reasons.
  • Separation: Use ReportFormatter, ReportFileWriter, and ReportEmailSender, each focused on one concern.
  • Cohesion: Members of an SRP-compliant class should support one closely related purpose, such as validating an order.
  • Practical limit: SRP does not mean every class has one method; it means the class has one coherent responsibility.

VIII. Open/Closed Principle (OCP)

OCP states that software entities should be open for extension but closed for modification.

A. Open/Closed Principle (OCP)

A stable abstraction should allow new behavior through new implementations rather than repeated edits to existing decision logic.

  • Violation: A shipping method contains if branches for "Air", "Sea", and every future method.
  • Extension: Define IShippingCalculator.Calculate(Order order) and add AirShippingCalculator or SeaShippingCalculator.
  • Polymorphic selection: A service can call the interface without knowing the concrete calculation.
  • Trade-off: Abstractions should be introduced around likely variation; unnecessary interfaces increase complexity without improving extensibility.

IX. Liskov Substitution Principle (LSP)

LSP requires that objects of a derived type can replace objects of their base type without breaking expected program behavior.

A. Liskov Substitution Principle (LSP)

Subtypes must honor the base contract, including valid inputs, outputs, and behavioral guarantees.

  • Contract example: If ReadOnlyFile derives from File but throws NotSupportedException from Write, callers expecting every File to be writable are misled.
  • Behavioral consistency: A subtype should not strengthen preconditions, such as accepting fewer valid inputs than its base type.
  • Design response: Separate writable and readable abstractions, for example IReadable and IWritable, when capabilities differ.
  • Result: Correct substitution reduces type checks and prevents runtime surprises.

X. Interface Segregation Principle (ISP)

ISP states that clients should not be forced to depend on methods they do not use.

A. Interface Segregation Principle (ISP)

Small, role-specific interfaces make implementations and consumers more focused.

  • Large interface: IMachine containing Print, Scan, Fax, and Staple burdens a simple printer with unsupported members.
  • Segregation: Define IPrinter.Print() and IScanner.Scan() separately.
  • Client dependency: A printing service depends only on IPrinter, so scanner changes do not affect it.
  • Design caution: Interfaces should be split by client needs and meaningful capabilities, not arbitrarily into single-method fragments.

XI. Dependency Inversion Principle (DIP)

DIP states that high-level modules should not depend directly on low-level modules; both should depend on abstractions.

A. Dependency Inversion Principle (DIP)

Business logic should express its required services through interfaces, while concrete infrastructure is supplied from outside.

  • Direct dependency: OrderService constructing SqlOrderRepository() tightly couples business logic to SQL storage.
  • Abstraction: Depend on IOrderRepository and inject the implementation.
CSHARP
  public OrderService(IOrderRepository repository)
  {
      _repository = repository;
  }
  • Dependency injection: A composition root, often application startup, maps IOrderRepository to SqlOrderRepository.
  • Testability: Tests can inject FakeOrderRepository, isolating order logic from a database.
  • Stable direction: High-level policy owns or references the abstraction; low-level details implement it.
  • Practical boundary: Use DIP at genuine change or infrastructure boundaries, such as databases, files, clocks, and external APIs.