Skip to content

Instantly share code, notes, and snippets.

@M-ElSherif
Last active April 12, 2022 17:54
Show Gist options
  • Save M-ElSherif/5f6906280d63026445a9fba86c5012a8 to your computer and use it in GitHub Desktop.
Save M-ElSherif/5f6906280d63026445a9fba86c5012a8 to your computer and use it in GitHub Desktop.
C# Notes

Asynchronous Programming

References

Async programming overview

Task-based asynchronous pattern (TAP)

Example

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace AsyncBreakfast
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Coffee cup = PourCoffee();
            Console.WriteLine("coffee is ready");

            var eggsTask = FryEggsAsync(2);
            var baconTask = FryBaconAsync(3);
            var toastTask = MakeToastWithButterAndJamAsync(2);

            var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask };
            while (breakfastTasks.Count > 0)
            {
                Task finishedTask = await Task.WhenAny(breakfastTasks);
                if (finishedTask == eggsTask)
                {
                    Console.WriteLine("eggs are ready");
                }
                else if (finishedTask == baconTask)
                {
                    Console.WriteLine("bacon is ready");
                }
                else if (finishedTask == toastTask)
                {
                    Console.WriteLine("toast is ready");
                }
                breakfastTasks.Remove(finishedTask);
            }

            Juice oj = PourOJ();
            Console.WriteLine("oj is ready");
            Console.WriteLine("Breakfast is ready!");
        }

        static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
        {
            var toast = await ToastBreadAsync(number);
            ApplyButter(toast);
            ApplyJam(toast);

            return toast;
        }

        private static Juice PourOJ()
        {
            Console.WriteLine("Pouring orange juice");
            return new Juice();
        }

        private static void ApplyJam(Toast toast) =>
            Console.WriteLine("Putting jam on the toast");

        private static void ApplyButter(Toast toast) =>
            Console.WriteLine("Putting butter on the toast");

        private static async Task<Toast> ToastBreadAsync(int slices)
        {
            for (int slice = 0; slice < slices; slice++)
            {
                Console.WriteLine("Putting a slice of bread in the toaster");
            }
            Console.WriteLine("Start toasting...");
            await Task.Delay(3000);
            Console.WriteLine("Remove toast from toaster");

            return new Toast();
        }

        private static async Task<Bacon> FryBaconAsync(int slices)
        {
            Console.WriteLine($"putting {slices} slices of bacon in the pan");
            Console.WriteLine("cooking first side of bacon...");
            await Task.Delay(3000);
            for (int slice = 0; slice < slices; slice++)
            {
                Console.WriteLine("flipping a slice of bacon");
            }
            Console.WriteLine("cooking the second side of bacon...");
            await Task.Delay(3000);
            Console.WriteLine("Put bacon on plate");

            return new Bacon();
        }

        private static async Task<Egg> FryEggsAsync(int howMany)
        {
            Console.WriteLine("Warming the egg pan...");
            await Task.Delay(3000);
            Console.WriteLine($"cracking {howMany} eggs");
            Console.WriteLine("cooking the eggs ...");
            await Task.Delay(3000);
            Console.WriteLine("Put eggs on plate");

            return new Egg();
        }

        private static Coffee PourCoffee()
        {
            Console.WriteLine("Pouring coffee");
            return new Coffee();
        }
    }
}

Create and Use Types

Value vs. Ref Types

  • Class variables are managed by reference
  • Struct variables are managed by value
using System;

namespace LISTING_2_1_Value_and_reference_types
{
    class Program
    {
        struct StructStore
        {
            public int Data { get; set; }
        }

        class ClassStore
        {
            public int Data { get; set; }
        }

        static void Main(string[] args)
        {
            StructStore xs, ys;
            ys = new StructStore();
            ys.Data = 99;
            xs = ys;
            xs.Data = 100;
            Console.WriteLine("xStruct: {0}", xs.Data);
            Console.WriteLine("yStruct: {0}", ys.Data);

            ClassStore xc, yc;
            yc = new ClassStore();
            yc.Data = 99;
            xc = yc;
            xc.Data = 100;
            Console.WriteLine("xClass: {0}", xc.Data);
            Console.WriteLine("yClass: {0}", yc.Data);

            Console.ReadKey();
        }
    }
}

Output

xStruct: 100
yStruct: 99
xClass: 100
yClass: 100

Value Types

Structures

Structures can contain methods, data values, properties and can have constructors. When comparing a structure with a class (which can also contain methods, data values, properties, and constructors) there are some differences between the classes and structures:

  • The constructor for a structure must initialize all the data members in the structure. Data members cannot be initialized in the structure.
  • It is not possible for a structure to have a parameterless constructor. it is possible for a structure to be created by calling a parameterless constructor on the structure type (eg. array), in which case all the elements of the structure are set to the default values for that type (numeric elements are set to zero and strings are set to null).
  • It is not possible to create a structure by extending a parent structure object.

Enums

Enumerated types are used in situations where the programmer wants to specify a range of values that a given type can have. For example, in the computer game you may want to represent three states of an alien: sleeping, attacking, or destroyed. You can use an integer variable to do this and adopt the convention that: the value 0 means sleeping, 1 means attacking, and 2 means destroyed

Unless specified otherwise, an enumerated type is based on the int type and the enumerated values are numbered starting at 0. You can modify this by adding extra information to the declaration of the enum. You would do this if you want to set particular values to be used in JSON and XML files when enumerated variables are stored. The code here creates an AlienState enum that is stored in a byte type, and has the given values for sleeping, attacking, and destroyed.

enum AlienState:
  byte {
    Sleeping = 1,
      Attacking = 2,
      Destroyed = 4
  };

Casting used to obtain numeric value held in an enum variable

Reference Types

...

Generic Types

Generic types are used extensively in C# collections, such as with the List and Dictionary classes. They allow you to create a List of any type of data, or a Dictionary of any type, indexed on any type.

using System;

namespace LISTING_2_6_Using_generic_types
{
    class Program
    {
        class MyStack<T> where T:class
        {
            int stackTop = 0;
            T[] items = new T[100];

            public void Push(T item)
            {
                if (stackTop == items.Length)
                    throw new Exception("Stack full");
                items[stackTop] = item;
                stackTop++;
            }

            public T Pop()
            {
                if (stackTop == 0)
                    throw new Exception("Stack empty");
                stackTop--;
                return items[stackTop];
            }
        }

        static void Main(string[] args)
        {
            MyStack<string> nameStack = new MyStack<string>();
            nameStack.Push("Rob");
            nameStack.Push("Mary");
            Console.WriteLine(nameStack.Pop());
            Console.WriteLine(nameStack.Pop());
            Console.ReadKey();
        }
    }
}

Generic Constraints

  • Referring to Example, MyStack can hold any type of data.
  • If you want to restrict it to only store reference types you can add a constraint on the possible types that T can represent
class MyStack<T> where T:class

Other constraints that can be used are listed below:

Constraint Behaviour
where T : class The type T must be a reference type
where T : struct The type T must be a value type.
where T : new() The type T must have a public, parameterless, constructor. Specify this constraint last if you are specifying a list of constraints
where T : <base class> The type T must be of type base class or derive from base class.
where T : <interface name> The type T must be or implement the specified interface. You can specify multiple interfaces.
where T : unmanaged The type T must not be a reference type or contain any members which are reference types.

Generic Interfaces

Variant Generic Interfaces

Creating Variant Generic Interfaces

    public interface IModelMapper <out T, in TU>
    {
        public IEnumerable<T> MapMultiple(IEnumerable<TU> results, bool queryBool = false);

        public T MapSingle(TU result, bool queryBool = false);

        public T MapModel(TU result, bool queryBool = false);
    }

Constructors

  • Can perform validation of parameters to ensure objects have valid info.
  • Could throw an exception if object created will have invalid values
class Alien {
  public int X;
  public int Y;
  public int Lives;
  public Alien(int x, int y) {
    if (x < 0 || y < 0)
      throw new ArgumentOutOfRangeException("Invalid position");
    X = x;
    Y = y;
    Lives = 3;
  }
}

Static Constructors

  • This is called once before the creation of the very first instance of the class.
  • A static constructor is a good place to load resources and initialize values that will be used by instances of the class. This can include the values of static members of the class

Static Variables

  • A static variable is a member of a type, but it is not created for each instance of a type
  • As an example of a situation where static is useful, you may decide that you want to set a maximum for the number of lives that an Alien is allowed to have. This is a value that should be stored once for all aliens. A static variable is a great place to store such a value, since it will be created once for all class instances
  • To make a static variable constant, must use const keyword or readonly keyword
class Alien {
  public static int Max_Lives = 99;
  public int X;
  public int Y;
  public int Lives;
  public Alien(int x, int y, int lives) {
    if (x < 0 || y < 0)
      throw new Exception("Invalid position");
    if (lives > Max_Lives)
      throw new Exception("Invalid lives");
    X = x;
    Y = y;
    Lives = lives;
  }
}

Methods

Extension Methods

  • Provide a way in which behaviors can be added to a class without needing to extend the class itself.
  • Extension methods are defined as static methods but are called by using instance method syntax.
  • Their first parameter specifies which type the method operates on. The parameter is preceded by the this modifier.
  • Extension methods are only in scope when you explicitly import the namespace into your source code with a using directive.
  • An extension method can never be used to replace an existing method in a class
using System;

namespace ExtensionMethods
{
    public static class MyExtensions
    {
        public static int LineCount(this string str)
        {
            return str.Split(new char[] { '\n' },
                             StringSplitOptions.RemoveEmptyEntries).Length;
        }
    }
}

namespace LISTING_2_13_Extension_method
{
    using ExtensionMethods;

    class Program
    {
        static void Main(string[] args)
        {
            string text = @"A rocket explorer called Wright,
Once travelled much faster than light,
He set out one day,
In a relative way,
And returned on the previous night";
            Console.WriteLine(text.LineCount());
            Console.ReadKey();
        }
    }
}

Named Parameters

static int ReadValue(
  int low, // lowest allowed value
  int high, // highest allowed value
  string prompt // prompt for the user
) {
  // method body...
}

...

x = ReadValue(low:1, high:100, prompt: "Enter your age: ");

Optional Parameters

  • Parameters must be provided after all of the required ones.
static int ReadValue(
  int low, // lowest allowed value
  int high, // highest allowed value
  string prompt = "" // prompt for the user
) {
  // method body...
}

...

x = readValue(25, 100);

Indexed Properties

  • Indexing mechanism to provide indexed property values
using System;

namespace LISTING_2_16_Indexed_properties
{
    class IntArrayWrapper
    {
        // Create an array to store the values
        private int[] array = new int[100];

        // Decleare an indexer property
        public int this[int i]
        {
            get { return array[i]; }
            set { array[i] = value; }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            IntArrayWrapper x = new IntArrayWrapper();

            x[0] = 99;

            Console.WriteLine(x[0]);

            Console.ReadKey();
        }
    }
}

Overloading and Overriding

Overloading

Overriding

  • Only methods that have been marked as virtual in the parent class can be overridden.
  • In the child class, overriden method marked as override
  • If a another child class inherits the first child class (now a parent class of the second child class), we can use the base keyword in child class to call a method in the parent class
class PrePaidInvoice: Invoice {
  public override void DoPrint() {
    base.DoPrint();
    Console.WriteLine("Hello from DoPrint in PrePaidInvoice");
  }
}

Boxing and Unboxing

  • Value to reference type → boxing
  • Reference to value type → unboxing -Each built-in C# value type ( int, float etc) has a matching C# type called its interface type to which it is converted when boxing is performed. Example, the interface type for int is int32.
namespace LISTING_2_21_Boxing_and_unboxing {
  class Program {
    static void Main(string[] args) {
      // the value 99 is boxed into an object
      object o = 99;
      // the boxed object is unboxed back into an int
      int oVal = (int) o;
      Console.WriteLine(oVal);
      Console.ReadKey();
    }
  }
}

Covert Types with System.Convert

  • The System.Convert class provides a set of static methods that can be used to perform type conversion between .NET types.

Example

int myAge = Convert.ToInt32("21");

The convert method will throw an exception if the string provided cannot be converted into an integer.

Dyanamic Types

  • The keyword dynamic is used to identify items for which the C# compiler should suspend static type checking. The compiler will then generate code that works with the items as described, without doing static checking to make sure that they are valid. Note that this doesn’t mean that a program using dynamic objects will always work; if the description is incorrect the program will fail at run time
  • A variable declared as dynamic is allocated a type that is inferred from the context in which it is used.

Example

            dynamic d = 99;
            d = d + 1;
            Console.WriteLine(d);

            d = "Hello";
            d = d + " Rob";
            Console.WriteLine(d);

            dynamic person = new ExpandoObject();

            person.Name = "Rob Miles";
            person.Age = 21;

            Console.WriteLine("Name: {0} Age: {1}", person.Name, person.Age);

            Console.ReadKey();

Expando Object

  • The ExpandoObject class allows a program to dynamically add properties to an object.
  • A program can add ExpandoObject properties to an ExpandoObject to create nested data structures. An ExpandoObject can also be queried using LINQ and can exposes the IDictionary interface to allow its contents to be queried and items to be removed. ExpandoObject is especially useful when creating data structures from markup languages, for example when reading a JSON or XML document.

Example

dynamic person = new ExpandoObject();

person.Name = "Rob Miles";
person.Age = 21;

Console.WriteLine("Name: {0} Age: {1}", person.Name, person.Age);

Encapsulation

Access Modifiers

  • private, public, protected

Encapsulation through properties

Example

    class Customer
    {
        private string _nameValue;

        public string Name
        {
            get
            {
                return _nameValue;
            }
            set
            {
                if (value == "")
                    throw new Exception("Invalid customer name");

                _nameValue = value;
            }
        }
    }
  • private data member _nameValue holds the value of the name that is being managed by the property. This value is called the backing value of the property. If you just want to implement a class member as a property, but don’t want to get control when the property is accessed, you can use auto-implemented properties.
  • The statement here creates an integer property called Age. The C# compiler automatically creates the backing values. If you want to add get and set behaviors and your own backing value later, you can do this.
public int Age {get; set;}

Encapsulation through Accessor Method

    class BankAccount
    {
        private decimal _accountBalance = 0;

        public void PayInFunds(decimal amountToPayIn)
        {
            _accountBalance = _accountBalance + amountToPayIn;
        }

        public bool WithdrawFunds(decimal amountToWithdraw)
        {
            if (amountToWithdraw > _accountBalance)
                return false;

            _accountBalance = _accountBalance - amountToWithdraw;
            return true;
        }

        public decimal GetBalance()
        {
            return _accountBalance;
        }
    }

Default Access Modifier

  • Default access to a member is private
  • Must explicitly state public to make it visible outside the type

Protected access

  • The protected access modifier makes a class member useable in any classes that extend the parent (base) class in which the member is declared

Internal access

  • The internal access modifier will make a member of a type accessible within the assembly in which it is declared. You can regard an assembly as the output of a C# project in Visual Studio. It can be either an executable program (with the language extension .exe) or a library of classes (with the language extension .dll). Internal access is most useful when you have a large number of cooperating classes that are being used to provide a particular library component. These classes may want to share members which should not be visible to programs that use the library. Using the access modifier internal allows this level of sharing.

Readonly access

  • The readonly access modifier will make a member of a type read only. The value of the member can only be set at declaration or within the constructor of the class.

Encapsulation using explicit interface implementation

  • When a class implements an interface it contains methods with signatures that match the ones specified in the interface. You can use an explicit interface implementation to make methods implementing an interface only visible when the object is accessed via an interface reference.
  • You can achieve this by making the implementation of the printing methods explicit, thus adding the interface name to the declaration of the method body.

Example

    interface IPrintable
    {
        string GetPrintableText(int pageWidth, int pageHeight);
        string GetTitle();
    }

    class Report : IPrintable
    {
        string IPrintable.GetPrintableText(int pageWidth, int pageHeight)
        {
            return "Report text to be printed";
        }

        string IPrintable.GetTitle()
        {
            return "Report title to be printed";
        }
    }

Interface methods not exposed

image

Interface methods exposed

image

Resolve duplicate method signatures by using explicit implementation

  • Sometimes a class may implement multiple interfaces, in which case it must contain all the methods defined in all the interfaces. This can lead to problems, in that two interfaces might contain a method with the same name
  • Use explicit implementation to overcome this
    interface IPrintable
    {
        string GetPrintableText(int pageWidth, int pageHeight);
        string GetTitle();
    }

    interface IDisplay
    {
        string GetTitle();
    }

    class Report : IPrintable, IDisplay
    {
        string IPrintable.GetPrintableText(int pageWidth, int pageHeight)
        {
            return "Report text to be printed";
        }

        string IPrintable.GetTitle()
        {
            return "Report title to be printed";
        }

        string IDisplay.GetTitle()
        {
            return "Report title to be displayed";
        }
    }

Create and Implement Class Heirachy

Designing Software Components and Interface

  • An interface in a C# program specifies how a software component could be used by another software component. So, instead of starting to build an application by designing classes. you should instead be thinking about describing their interfaces (what each software component will do). How the component performs its function can be encapsulated inside the component

  • Benefit is "decoupling" of class that utilizes an object that implements an interface

Example

    interface IPrintable
    {
        string GetPrintableText(int pageWidth, int pageHeight);
        string GetTitle();0
    }
    class Report : IPrintable
    {
        public string GetPrintableText(int pageWidth, int pageHeight)
        {
            return "Report text";
        }

        public string GetTitle()
        {
            return "Report title";
        }
    }

    class ConsolePrinter
    {
        public void PrintItem(IPrintable item)
        {
            Console.WriteLine(item.GetTitle());
            Console.WriteLine(item.GetPrintableText(pageWidth: 80, pageHeight: 25));
        }
    }

Printer is decoupled from object being printed

  • Consider classes in terms of they can do rather than what they are, by implementing and designing interfaces
  • Create reference variables that refer to objects in terms of interfaces they implement rather than the particular type they are
IAccount account = new BankAccount ();
account.PayInFunds(50);
Console.WriteLine("Balance: " + account.GetBalance());

Inheritance from base class

  • A class hierarchy is used when you have an application that must manipulate items that are part of a particular group

The is and as operators

  • The is operator determines if the type of a given object is in a particular class hierarchy or implements a specified interface
if (x is IAccount)
Console.WriteLine("this object can be used as an account");

The as operator takes a reference and a type and returns a reference of the given type, or null if the reference cannot be made to refer to the object.

IAccount y = x as IAccount;
  • A cast will throw an exception if it can't be performed
  • using as will let it return null instead

Overriding methods

Example

Example

```C#
    public interface IAccount
    {
        void PayInFunds(decimal amount);
        bool WithdrawFunds(decimal amount);
        decimal GetBalance();
    }

    public class BankAccount : IAccount
    {
        protected decimal _balance = 0;

        public virtual bool WithdrawFunds(decimal amount)
        {
            if (_balance < amount)
            {
                return false;
            }
            _balance = _balance - amount;
            return true;
        }

        void IAccount.PayInFunds(decimal amount)
        {
            _balance = _balance + amount;
        }

        decimal IAccount.GetBalance()
        {
            return _balance;
        }
    }

    public class BabyAccount : BankAccount, IAccount
    {
        public override bool WithdrawFunds(decimal amount)
        {
            if (amount > 10)
            {
                return false;
            }

            if (_balance < amount)
            {
                return false;
            }
            _balance = _balance - amount;
            return true;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            IAccount b = new BabyAccount();
            b.PayInFunds(50);
            if (b.WithdrawFunds(10))
                Console.WriteLine("Withdraw succeeded");
            else
                Console.WriteLine("Withdraw failed");

            Console.ReadKey();
        }
    }
  • The C# compiler needs to know if a method is going to be overridden. This is because it must call an overridden method in a slightly different way from a “normal” one. Done by assinging virtual keyword to method that will be overriden in child classes
  • The C# language does not allow the overriding of explicit implementations of interface methods.

Using base method

Example

    public class BabyAccount : BankAccount, IAccount
    {
        public override bool WithdrawFunds(decimal amount)
        {
            if (amount > 10)
            {
                return false;
            }
            else
            {
                return base.WithdrawFunds(amount);
            }
        }
    }

Replacing methods in base classes

  • replace a method in a base class by simply creating a new method in the child class. No overriding, just supply a new version of the method.
  • replacement method is not able to use base keyword
public class BabyAccount: BankAccount, IAccount
{
  public new bool WithdrawFunds(decimal amount) {
    if (amount > 10) {
      return false;
    }
    if (_balance < amount) {
      return false;
    }
    _balance = _balance - amount;
    return true;
  }
}

Stopping overriding

  • You can only seal an overriding method and sealing a method does not prevent a child class from replacing a method in a parent.
  • You can also mark a class as sealed. This means that the class cannot be extended, so it cannot be used as the basis for another class
public sealed class BabyAccount : CustomerAccount,IAccount
{
  .....
}

Constructors and class heirachies

  • Creating a child class instance involves creating an instance of the base class aka. invokes constructor of base class
public class BabyAccount: BankAccount, IAccount
{
  public BabyAccount(int initialBalance): base(initialBalance) {}
}

Abstract methods and classes

  • Overriding can be used to force a set of behaviors on items in a class hierarchy.
  • abstract keyword is used on a method or class variable in the base class, which has to be overriden in child classes

Abstract classes and interfaces

  • abstract classes are different in that they can contain fully implemented methods alongside the abstract ones. This can be useful because it means you don’t have to repeatedly implement the same methods in each of the components that implement a particular interface.
  • A class can only inherit from one parent, so it can only pick up the behaviors of one class.
  • Some languages support multiple inheritance, where a class can inherit from multiple parents. C# does not allow this.

References in class hierarchies

  • Reference to a base class in a class hierarchy can refer to an instance of any of the classes that inherits from that base class.
  • However, the reverse is not true. This is because the child class may have added extra behaviors to the parent class
  • Preferred to manage references to objects in terms of the interfaces than the type of the particular object. This is much more flexible, in that you’re not restricted to a particular type of object when developing the code

.NET Interfaces

IComparable

  • The IComparable interface is used by .NET to determine the ordering of objects when they are sorted
public interface IComparable {
  //
  // Summary:
  // Compares the current instance with another object of the same type and
  returns
  // an integer that indicates whether the current instance precedes, follows, or
  // occurs in the same position in the sort order as the other object.
  //
  // Parameters:
  // obj:
  // An object to compare with this instance.
  //
  // Returns:
  // A value that indicates the relative order of the objects being compared. The
  // return value has these meanings: Value Meaning Less than zero This instance
  precedes
  // obj in the sort order. Zero This instance occurs in the same position in the
  // sort order as obj. Greater than zero This instance follows obj in the sort
  order.
  //
  // Exceptions:
  // T:System.ArgumentException:
  // obj is not the same type as this instance.
  int CompareTo(object obj);
}

Example

    public interface IAccount
    {
        void PayInFunds(decimal amount);
        bool WithdrawFunds(decimal amount);
        decimal GetBalance();
    }

    public class BankAccount : IAccount, IComparable
    {
        private decimal balance;

        public virtual bool WithdrawFunds(decimal amount)
        {
            if (balance < amount)
            {
                return false;
            }
            balance = balance - amount;
            return true;
        }

        void IAccount.PayInFunds(decimal amount)
        {
            balance = balance + amount;
        }

        decimal IAccount.GetBalance()
        {
            return balance;
        }

        public int CompareTo(object obj)
        {
            // if we are being compared with a null object we are definitely after it
            if (obj == null) return 1;

            // Convert the object reference into an account reference
            IAccount account = obj as IAccount;

            // as generates null if the conversion fails
            if (account == null)
                throw new ArgumentException("Object is not an account");

            // use the balance value as the basis of the comparison
            return this.balance.CompareTo(account.GetBalance());
        }

        public BankAccount(decimal initialBalance)
        {
            balance = initialBalance;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // Create 20 accounts with random balances
            List<IAccount> accounts = new List<IAccount>();
            Random rand = new Random(1);
            for(int i=0; i<20; i++)
            {
                IAccount account = new BankAccount(rand.Next(0, 10000));
                accounts.Add(account);
            }

            // Sort the accounts
            accounts.Sort();

            // Display the sorted accounts
            foreach(IAccount account in accounts)
            {
                Console.WriteLine(account.GetBalance());
            }

            Console.ReadKey();
        }
    }
}

Typed IComparable

  • Can be used to create a CompareTo that only accepts parameters of a specified type.
  • Avoids casting and throwing invalid type exception at runtime
public interface IAccount: IComparable <IAccount>
{
  void PayInFunds(decimal amount);
  bool WithdrawFunds(decimal amount);
  decimal GetBalance();
}
public class BankAccount: IAccount, IComparable < BankAccount >
{
  private decimal balance;
  public int CompareTo(IAccount account)
  {
    // if we are being compared with a null object we are definitely after it
    if (account == null) return 1;
    // use the balance value as the basis of the comparison
    return this.balance.CompareTo(account.GetBalance());
  }
}

IEnumerable

  • The IEnumerator interface defines the basic low-level protocol by which elements in a collection are traversed—or enumerated—in a forward-only manner. Its declaration is as follows:
public interface IEnumerator
{
  bool MoveNext();
  object Current { get; }
  void Reset();
}
  • Collections do not usually implement enumerators; instead, they provide enumerators, via the interface IEnumerable:
public interface IEnumerable
{
  IEnumerator GetEnumerator();
}
  • IEnumerator<int> interface means it contains a call of GetEnumerator to get an enumerator from it
  • Enumerable interface means it can be enumerated
    class EnumeratorThing: IEnumerator<int> , IEnumerable {
      int count;
      int limit;

      public int Current {
        get {
          return count;
        }
      }

      object IEnumerator.Current {
        get {
          return count;
        }
      }

      public EnumeratorThing(int limit) {
        count = 0;
        this.limit = limit;
      }

      public void Dispose() {}

      public bool MoveNext() {
        if (++count == limit)
          return false;
        else
          return true;
      }

      public void Reset() {
        count = 0;
      }

      public IEnumerator GetEnumerator() {
        return this;
      }
    }

    class Program {
      static void Main(string[] args) {
        EnumeratorThing e = new EnumeratorThing(10);

        foreach(int i in e) {
          Console.WriteLine(i);
        }

        Console.ReadKey();
      }
    }

Yield

  • To make it easier to create iterators C# includes the yield keyword.
  • The keyword yield is followed by the return keyword and precedes the value to be returned for the current iteration.
    • The C# compiler generates all the Current and MoveNext behaviors that make the iteration work, and also records the state of the iterator method so that the iterator method resumes at the statement following the yield statement when the next iteration is requested.

Example

    class EnumeratorThing : IEnumerable<int>
    {
        private int limit;

        public IEnumerator<int> GetEnumerator()
        {
            for (int i = 1; i < 10; i++)
                yield return i;
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        public EnumeratorThing(int limit)
        {
            this.limit = limit;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            EnumeratorThing e = new EnumeratorThing(10);

            foreach (int i in e)
            {
                Console.WriteLine(i);
            }

            Console.ReadKey();
        }
    }

IDisposable

// Summary:
// Provides a mechanism for releasing unmanaged resources.
public interface IDisposable {
  //
  // Summary:
  // Performs applicationdefined
  tasks associated with freeing, releasing, or
  resetting
  // unmanaged resources.
  void Dispose();
}
  • To trigger, either call Dispose() directly or make use of C# using
    class CrucialConnection : IDisposable
   {
       public void Dispose()
       {
           Console.WriteLine("Dispose called");
       }

       public void ThrowException()
       {
           throw new Exception("Bang");
       }

   }
   class Program
   {
       static void Main(string[] args)
       {
           using (CrucialConnection c = new CrucialConnection())
           {
               // do something with the crucial connection
           }

           Console.ReadKey();
       }
   }

Reflection

Events and Callbacks

Event handlers

  • An object can be made to publish events to which other objects can subscribe. Components of a solution that communicate using events in this way are described as loosely coupled. The only thing one component has to know about the other is the design of the publish and subscribe mechanism.

Action Delegate

  • NET libraries provide a number of pre-defined delegate types. One of them is the Action delegate
  • There are a number of pre-defined Action delegate types. The simplest Action delegate represents a reference to a method that does not return a result (the method is of type void) and doesn’t accept any parameters. You can use an Action to create a binding point for subscribers

Event Subscrubers

  • Subscribers bind to a publisher by using the += operator. The += operator is overloaded to apply between a delegate and a behavior
  • Delegates added to a published event are called on the same thread as the thread publishing the event. If a delegate blocks this thread, the entire publication mechanism is blocked. This means that a malicious or badly written subscriber has the ability to block the publication of events. This is addressed by the publisher starting an individual task to run each of the event subscribers. The Delegate object in a publisher exposes a method called GetInvocationList, which can be used to get a list of all the subscribers.

Example

    class Alarm
    {
        // Delegate for the alarm event
        public Action OnAlarmRaised { get; set; }

        // Called to raise an alarm
        public void RaiseAlarm()
        {
            // Only raise the alarm if someone has
            // subscribed.
            OnAlarmRaised?.Invoke();
        }
    }

    class Program
    {
        // Method that must run when the alarm is raised
        static void AlarmListener1()
        {
            Console.WriteLine("Alarm listener 1 called");
        }

        // Method that must run when the alarm is raised
        static void AlarmListener2()
        {
            Console.WriteLine("Alarm listener 2 called");
        }

        static void Main(string[] args)
        {
            // Create a new alarm
            Alarm alarm = new Alarm();

            // Connect the two listener methods
            alarm.OnAlarmRaised += AlarmListener1;
            alarm.OnAlarmRaised += AlarmListener2;

            alarm.RaiseAlarm();
            Console.WriteLine("Alarm raised");
            Console.ReadKey();
        }
    }

Unsubscribing from a delegate

  • The -= method is used to unsubscribe from events
    class Alarm
    {
        // Delegate for the alarm event
        public Action OnAlarmRaised { get; set; }

        // Called to raise an alarm
        public void RaiseAlarm()
        {
            // Only raise the alarm if someone has
            // subscribed.
            OnAlarmRaised?.Invoke();
        }
    }

    class Program
    {
        // Method that must run when the alarm is raised
        static void AlarmListener1()
        {
            Console.WriteLine("Alarm listener 1 called");
        }

        // Method that must run when the alarm is raised
        static void AlarmListener2()
        {
            Console.WriteLine("Alarm listener 2 called");
        }

        static void Main(string[] args)
        {
            // Create a new alarm
            Alarm alarm = new Alarm();

            // Connect the two listener methods
            alarm.OnAlarmRaised += AlarmListener1;
            alarm.OnAlarmRaised += AlarmListener2;

            alarm.RaiseAlarm();
            Console.WriteLine("Alarm raised");

            alarm.OnAlarmRaised -= AlarmListener1;

            alarm.RaiseAlarm();
            Console.WriteLine("Alarm raised");

            Console.ReadKey();
        }
    }

Func delegate

  • The Func types provide a range of delegates for methods that accept values and return results. There are versions of the Func type that accept up to 16 input items.
  • The add method here accepts two integers and returns an integer as the result.

Example

Func<int,int,int> add = (a, b) => a + b;

Predicate delegate

  • The Predicate built in delegate type lets you create code that takes a value of a particular type and returns true or false.

Example

Predicate<int> dividesByThree = (i) => i % 3 == 0;

....

TO BE CONTINUED

....

CODE EXAMPLES

Unit Testing - NUnit

Alternative way to provide test data that changes based on environment

using System.Collections.Generic;
using NUnit.Framework;
using PlaceHolder.Integration.Client.Extensions.eClinicalWorks.Services;
using PlaceHolder.Integration.Client.Extensions.eClinicalWorks.Services.Entities.Patient;
using PlaceHolder.Integration.Client.Extensions.eClinicalWorks.Services.Entities.PatientSearch;

namespace PlaceHolder.Integration.Client.Extensions.eClinicalWorks.Tests.Integration.Endpoints
{
    [TestFixture]
    [Category("ExternalServiceDepend")]
    public class SmartSearchIntegrationTests : IntegrationTestsBase
    {
        private IplaceCloudService _placeCloudService;
        private static string _basePatientId;
        private static PatientResult _basePatientResult;
        private Dictionary<SmartSearchQuery, string> _testQueries;

        [OneTimeSetUp]
        public void OneTimeSetup()
        {
            _placeCloudService = GetplaceCloudService();
            _basePatientId = GetOrCreatePatientId(option: TestContextOption.Class);
            _basePatientResult = _placeCloudService.GetPatient(_basePatientId);

            _testQueries = new Dictionary<SmartSearchQuery, string>()
            {
                { SmartSearchQuery.ChartNumber, _basePatientResult.Patient.Id },
                { SmartSearchQuery.LastName, _basePatientResult.Patient.LastName },
                { SmartSearchQuery.LastNameFirstName, _basePatientResult.Patient.LastName + "," + _basePatientResult.Patient.FirstName },
                { SmartSearchQuery.DateOfBirth, _basePatientResult.Patient.DateOfBirth.Raw }
            };
        }

        [TestCase(SmartSearchQuery.ChartNumber, TestName = "Search query: chart number only")]
        [TestCase(SmartSearchQuery.LastName, TestName = "Search query: last name only")]
        [TestCase(SmartSearchQuery.LastNameFirstName, TestName = "Search query: last name, first name")]
        [TestCase(SmartSearchQuery.DateOfBirth, TestName = "Search query: date of birth only")]
        public void SmartSearch_Imports_Patient(SmartSearchQuery queryKey)
        {
            if (!_testQueries.TryGetValue(queryKey, out var queryValue))
            {
                return;
            }

            SmartSearchResult smartSearchResult = _placeCloudService.SmartSearch(queryValue);
            SmartSearchPatient smartSearchPatient = smartSearchResult?.Patients?.Find(p => p.Id == _basePatientId);

            Assert.AreEqual(_basePatientResult.Patient.Id, smartSearchPatient.Id);
            Assert.AreEqual(_basePatientResult.Patient.LastName, smartSearchPatient.LastName);
            Assert.AreEqual(_basePatientResult.Patient.FirstName, smartSearchPatient.FirstName);
            Assert.AreEqual(_basePatientResult.Patient.DateOfBirth.Raw, smartSearchPatient.DateOfBirth.Raw);
        }
    }

    public enum SmartSearchQuery
    {
        ChartNumber,
        LastName,
        LastNameFirstName,
        DateOfBirth
    }

}

Serialize and Deserialize

TestDataManager to dynamically deal with json test data

    internal class TestDataManager
    {
        public IList<string> ApprovedSiteServiceKeys => _approvedSiteServiceKeys;

        private Dictionary<string, dynamic> _testData;

        private Dictionary<string, dynamic> _sharedTestData;

        private Dictionary<string, dynamic> _globalTestData;

        private List<string> _approvedSiteServiceKeys;

        private const string TestEnvironmentParam = "TestEnvironment";

        private readonly string _testEnvironment = TestContext.Parameters.Get(TestEnvironmentParam,
            ConfigurationManager.AppSettings["DefaultTestEnvironment"]);

        private readonly string _testDataPath = ConfigurationManager.AppSettings["IntegrationTestDataPath"];

        private readonly string _solutionTestDataPath = ConfigurationManager.AppSettings["SolutionIntegrationTestDataPath"];

        private string TestDataDirectory =>
            Path.Combine(TestContext.CurrentContext.TestDirectory, _testDataPath);

        private string EnvironmentDirectory =>
            Path.Combine(TestContext.CurrentContext.TestDirectory, _testDataPath, _testEnvironment);

        private string SolutionEnvironmentDirectory =>
            Path.Combine(TestContext.CurrentContext.TestDirectory, _solutionTestDataPath, _testEnvironment);

        private string SharedEnvironmentDirectory =>
            Path.Combine(TestContext.CurrentContext.TestDirectory, _testDataPath, "Shared");

        private string SolutionSharedEnvironmentDirectory =>
            Path.Combine(TestContext.CurrentContext.TestDirectory, _solutionTestDataPath, "Shared");

        private readonly NewtonsoftSerializer _serializer = new NewtonsoftSerializer();

        public TestDataManager()
        {
            LoadTestData();
        }

        public T GetTestData<T>(string key, IDictionary<string, string> macros = null)
        {
            if (key == null)
            {
                throw new ArgumentNullException(nameof(key));
            }

            var settings = new JsonSerializerSettings { DateParseHandling = DateParseHandling.None };

            string testCaseName = TestContext.CurrentContext.Test.MethodName;

            try
            {
                // Check Environment/TestClassName.json
                if (_testData != null && !string.IsNullOrWhiteSpace(testCaseName) && _testData.ContainsKey(testCaseName))
                {
                    string testCaseText = ReplaceMacros(_testData?[testCaseName].ToString(), macros);

                    dynamic testCaseData = JsonConvert.DeserializeObject<Dictionary<string, dynamic>>(testCaseText, settings);

                    return JsonConvert.DeserializeObject<T>(testCaseData[key].ToString(), settings);
                }
            }
            catch (KeyNotFoundException)
            {
                Console.WriteLine($"Key: {key} not found in test specific data. Checking environment globals...");
            }

            // Check Environment/Globals.json
            if (_globalTestData != null && _globalTestData.ContainsKey(key))
            {
                string testDataText = ReplaceMacros(_globalTestData[key].ToString(), macros);
                return JsonConvert.DeserializeObject<T>(testDataText, settings);
            }

            if (string.IsNullOrWhiteSpace(testCaseName))
            {
                throw new InvalidOperationException("Test case cannot be null or empty.");
            }

            // Check Shared/TestClassName.json
            if (_sharedTestData != null && _sharedTestData.ContainsKey(testCaseName))
            {
                string testCaseText = ReplaceMacros(_sharedTestData?[testCaseName].ToString(), macros);

                dynamic testCaseData = JsonConvert.DeserializeObject<Dictionary<string, dynamic>>(testCaseText, settings);

                return JsonConvert.DeserializeObject<T>(testCaseData[key].ToString(), settings);
            }

            throw new InvalidOperationException("Test data was not found");
        }

        public void ValidateResponse<T>(Func<T> requestFunc, string testDataKey, IDictionary<string, string> macros = null,
            Action<string, string> validation = null)
        {
            T response = requestFunc();
            string responseText = ToJson(response);

            T expectedResponse = GetTestData<T>(testDataKey, macros);
            string expectedResponseText = ToJson(expectedResponse);

            if (validation is null)
            {
                Assert.AreEqual(expectedResponseText, responseText);
                return;
            }

            validation(expectedResponseText, responseText);
        }

        public string ToJson<T>(T data)
        {
            return _serializer.SerializeObject(data);
        }

        #region Developer use only

        public void UpdateResponse<T>(Func<T> requestFunc, string testDataKey)
        {
            T response = requestFunc();
            string responseText = ToJson(response);

            UpdateTestData(testDataKey, responseText);
        }

        public void AddResponse<T>(Func<T> requestFunc, string testDataKey, TestDataType type)
        {
            T response = requestFunc();
            string responseText = ToJson(response);

            AddTestData(testDataKey, responseText, type);
        }

        public void AddTestData(string key, string value, TestDataType type)
        {
            if (key == null)
            {
                throw new ArgumentNullException(nameof(key));
            }

            string testCaseName = TestContext.CurrentContext.Test.MethodName;
            if (string.IsNullOrWhiteSpace(testCaseName))
            {
                throw new InvalidOperationException("Test case cannot be null or empty.");
            }

            var settings = new JsonSerializerSettings { DateParseHandling = DateParseHandling.None };

            switch (type)
            {
                case TestDataType.TestSpecific:
                    // Check Environment/TestClassName.json
                    if (_testData != null)
                    {
                        if (!_testData.ContainsKey(testCaseName))
                        {
                            _testData.Add(testCaseName, string.Empty);
                        }

                        string testCaseText = _testData?[testCaseName].ToString();

                        Dictionary<string, dynamic> testCaseData =
                            JsonConvert.DeserializeObject<Dictionary<string, dynamic>>(testCaseText, settings)
                            ?? new Dictionary<string, dynamic>();

                        testCaseData.Add(key, JsonConvert.DeserializeObject<dynamic>(value));

                        // Save to file
                        _testData[testCaseName] = testCaseData;

                        string testDataText = ToJson(_testData);

                        string filePath = Path.Combine(SolutionEnvironmentDirectory, GetClassName() + ".json");

                        File.WriteAllText(filePath, testDataText);
                    }
                    break;
                case TestDataType.EnvGlobals:
                    // Check Environment/Globals.json
                    if (_globalTestData != null)
                    {
                        // Save to file
                        _globalTestData.Add(key, JsonConvert.DeserializeObject<dynamic>(value));

                        string globalTestDataText = ToJson(_globalTestData);

                        string filePath = Path.Combine(SolutionEnvironmentDirectory, "Globals.json");

                        File.WriteAllText(filePath, globalTestDataText);
                    }
                    break;
                case TestDataType.Shared:
                    // Check Shared/TestClassName.json
                    if (_sharedTestData != null)
                    {
                        if (!_sharedTestData.ContainsKey(testCaseName))
                        {
                            _sharedTestData.Add(testCaseName, string.Empty);
                        }

                        string testCaseText = _sharedTestData?[testCaseName].ToString();

                        Dictionary<string, dynamic> testCaseData = JsonConvert.DeserializeObject<Dictionary<string, dynamic>>(testCaseText, settings)
                                                                   ?? new Dictionary<string, dynamic>();

                        testCaseData.Add(key, JsonConvert.DeserializeObject<dynamic>(value));

                        // Save to file
                        _sharedTestData[testCaseName] = testCaseData;

                        string sharedTestDataText = ToJson(_sharedTestData);

                        string filePath = Path.Combine(SolutionSharedEnvironmentDirectory, GetClassName() + ".json");

                        File.WriteAllText(filePath, sharedTestDataText);
                    }
                    break;
                default:
                    throw new ArgumentOutOfRangeException(nameof(type), type, null);
            }
        }

        #endregion

        private void LoadTestData()
        {
            var settings = new JsonSerializerSettings { DateParseHandling = DateParseHandling.None };

            string filePath = Path.Combine(EnvironmentDirectory, GetClassName() + ".json");

            if (File.Exists(filePath))
            {
                string testDataText = File.ReadAllText(filePath);
                _testData = JsonConvert.DeserializeObject<Dictionary<string, dynamic>>(testDataText, settings);
            }

            filePath = Path.Combine(EnvironmentDirectory, "Globals.json");
            if (File.Exists(filePath))
            {
                string globalTestDataText = File.ReadAllText(filePath);
                _globalTestData = JsonConvert.DeserializeObject<Dictionary<string, dynamic>>(globalTestDataText, settings);

            }

            filePath = Path.Combine(SharedEnvironmentDirectory, GetClassName() + ".json");
            if (File.Exists(filePath))
            {
                string sharedTestDataText = File.ReadAllText(filePath);
                _sharedTestData = JsonConvert.DeserializeObject<Dictionary<string, dynamic>>(sharedTestDataText, settings);
            }

            filePath = Path.Combine(TestDataDirectory, "ApprovedSiteServiceKeys.json");
            if (!File.Exists(filePath))
            {
                throw new InvalidOperationException($"Did not find required file '{filePath}'");
            }
            else
            {
                string approvedSiteSvcKeysText = File.ReadAllText(filePath);
                _approvedSiteServiceKeys = JsonConvert.DeserializeObject<List<string>>(approvedSiteSvcKeysText, settings);
            }
        }

        private void UpdateTestData(string key, string value)
        {
            if (key == null)
            {
                throw new ArgumentNullException(nameof(key));
            }

            var settings = new JsonSerializerSettings { DateParseHandling = DateParseHandling.None };

            string testCaseName = TestContext.CurrentContext.Test.MethodName;
            if (string.IsNullOrWhiteSpace(testCaseName))
            {
                throw new InvalidOperationException("Test case cannot be null or empty.");
            }

            // Check Environment/TestClassName.json
            if (_testData != null && _testData.ContainsKey(testCaseName))
            {
                string testCaseText = _testData?[testCaseName].ToString();

                Dictionary<string, dynamic> testCaseData = JsonConvert.DeserializeObject<Dictionary<string, dynamic>>(testCaseText, settings)
                                                           ?? new Dictionary<string, dynamic>();

                if (testCaseData.ContainsKey(key))
                {
                    testCaseData[key] = JsonConvert.DeserializeObject<dynamic>(value);
                }
                else
                {
                    throw new InvalidOperationException($"Key: {key} was not found in test case: {testCaseName}");
                }

                // Save to file
                _testData[testCaseName] = testCaseData;

                string testDataText = ToJson(_testData);

                string filePath = Path.Combine(SolutionEnvironmentDirectory, GetClassName() + ".json");

                File.WriteAllText(filePath, testDataText);
                return;
            }

            // Check Environment/Globals.json
            if (_globalTestData != null && _globalTestData.ContainsKey(key))
            {
                // Save to file
                _globalTestData[key] = JsonConvert.DeserializeObject<dynamic>(value);

                string globalTestDataText = ToJson(_globalTestData);

                string filePath = Path.Combine(SolutionEnvironmentDirectory, "Globals.json");

                File.WriteAllText(filePath, globalTestDataText);
                return;
            }

            // Check Shared/TestClassName.json
            if (_sharedTestData != null && _sharedTestData.ContainsKey(testCaseName))
            {
                string testCaseText = _sharedTestData?[testCaseName].ToString();

                Dictionary<string, dynamic> testCaseData = JsonConvert.DeserializeObject<Dictionary<string, dynamic>>(testCaseText, settings)
                                                           ?? new Dictionary<string, dynamic>();

                if (testCaseData.ContainsKey(key))
                {
                    testCaseData[key] = JsonConvert.DeserializeObject<dynamic>(value);
                }
                else
                {
                    throw new InvalidOperationException($"Key: {key} was not found in test case: {testCaseName}");
                }

                // Save to file
                _sharedTestData[testCaseName] = testCaseData;

                string sharedTestDataText = ToJson(_sharedTestData);

                string filePath = Path.Combine(SolutionSharedEnvironmentDirectory, GetClassName() + ".json");

                File.WriteAllText(filePath, sharedTestDataText);
                return;
            }

            throw new InvalidOperationException("Test data file or key was not found.");
        }

        private static string ReplaceMacros(string text, IDictionary<string, string> macros)
        {
            if (macros is null || text is null)
            {
                return text;
            }

            return macros.Aggregate(text, (current, macro) => current.Replace('<' + macro.Key + '>', macro.Value));
        }

        private static string GetClassName()
        {
            string className = TestContext.CurrentContext.Test.ClassName;
            if (string.IsNullOrEmpty(className))
            {
                throw new InvalidOperationException(nameof(className) + " cannot be null or empty.");
            }

            return className.Split('.').Last();
        }
    }

    public enum TestDataType
    {
        TestSpecific,
        EnvGlobals,
        Shared
    }

File I/O

Get File Path

  • Ensure that the file is copied to output directory (VS studio file properties) first
namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            var appPath = AppDomain.CurrentDomain.BaseDirectory;
            var path = Path.Combine(
                appPath,
                @"Testfiles\TextFile1.txt");

            if (File.Exists(path))
            {
                Console.WriteLine("Exists");
            }
            else
            {
                Console.WriteLine("Does not exist");
            }
        }
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment