C# .NET Application Design Considerations

Design plays a key role in any software application development process. Proper design is a major factor that contributes to the scalability and performance of any application.

Efficient Resource Management

Resources are very critical for the existence and survival of any application. Examples of resources include objects, files, connections, handles, streams and so on. Hence proper handling and cleanup of such resources is critical for the better performance of a system.

Here are points to consider:

  • Should properly handle the object creation and cleanup if needed.
  • In .NET some objects offer a Close() method to do proper handling of resources. Some of those are database connection, streams and so on.
  • .NET provides proper structured blocks that can be used in your code to enforce the cleanup if situation arises. For example, finally blocks or using statements can be used to ensure that resources are closed or released properly and in a timely fashion.

Considerations for Crossings the Application Boundary

Applications live and run in their own territory defined by the machine, process or Application Domains they are hosted in or deployed on. In other words, if two applications communicate with each other across machines, processes or app domain then there is a significant impact or improvement in the performance of the application.

  • Cross application domain. Since in .NET a process is optimized to have multiple application domains that can then host an application inside those app domains. This is the most efficient boundary to cross because it is within the context of a single process.

    AppDesign1.jpg
     
  • Cross process. Crossing a process boundary significantly impacts performance. You should do so only when absolutely necessary. For example, you might determine that an Enterprise Services server application is required for security and fault tolerance reasons.

    AppDesign2.jpg
     
  • Cross machine. Crossing a machine boundary is the most expensive boundary to cross, due to network latency and marshaling overhead. Before introducing a remote server into your design, you need to consider the relative tradeoffs including performance, security, and administration.

    AppDesign3.jpg

Single Large Assemblies or Multiple Smaller Assemblies

In .NET the assembly is the unit of deployment; an application existd in the form of an assembly only. For any application you have built, compilation only produces an assembly. All the DLLs your project refers to are .NET Assemblies only.

When working on designing and architecting a solution it is critical to consider that the various functionalities, classes and interfaces and so on should be a part of one single chunky assembly or should be divided across multiple smaller assemblies.

Here are points to consider:

  • To help reduce your application's working set (in simple terms set of memory pages required to host the application in the process) , you should prefer single larger assemblies rather than multiple smaller assemblies. If you have several assemblies that are always loaded together, you should combine them and create a single assembly.
  • Reasons to avoid multiple smaller assemblies:
    - Since you have multiple assemblies consider the cost required in loading metadata for multiple smaller assemblies.
    - JIT compile time will be much greater as per count of assemblies.
    - Security checks needed for multiple assemblies.
  • At times, the project architecture demands splitting assemblies into multiple ones; for example, for versioning and deployment reasons. If you need to ship types separately, you may need separate assemblies. If you have valid scenario(s) feel free to do so.

Code Refactoring by Logical Layers

You can read my article on Logical and Physical Separation here

Code refactoring suggestions are the most common comment developers receive during their code review. Hence, we usually follow the best practices of Separation of Concerns and so on. .

Here are points to consider:

  • Class's internal design needs to be considered to decide how you factor code into separate methods.
  • Better code refactoring boost up the ease for tuning to improves performance; simplify maintainability, and adding new functionality.
  • Like many things code refactoring needs a proper end where you stop further refactoring. In other words clearly re-factored code improves maintainability but a developer needs to be cautious of over abstraction and dividing functionality into many layers.
  • Try to keep your application's design simple, effective and efficient, as much as you can.

Threads are a Resource worth Sharing

Many application use threads internally and behind the scenes, that is good and doesn't cause much noise. The problem starts when we take control in our own hands and don't follow the right practices.

Here are points to consider:

  • Threads use both managed and unmanaged resources and are expensive to initialize. The following code shows a new thread being created and maintained for each request.

    private void Page_Load(object sender, System.EventArgs e)
    {
         if (Page.IsPostBack)
         {
              // Create and start a thread
              ThreadStart newTS = new ThreadStart(CalFunc);
              Thread th = new Thread(newTS);
               newTS.Start();
               ......
    }
     
  • This may result in the processor spending most of its time performing thread switches; it also places increased pressure on the garbage collector to clean up resources.
  • Use the CLR thread pool to execute thread-based work to avoid expensive thread initialization. The following code shows a method being executed using a thread from the thread pool.

    WaitCallback methodTarget = new WaitCallback( myClass.UpdateCache );
    ThreadPool.QueueUserWorkItem( methodTarget );
     
  • When QueueUserWorkItem is called, the method is queued for execution and the calling thread returns and continues execution. The ThreadPool class uses a thread from the application's pool to execute the method passed in the callback as soon as a thread is available.