Things to Consider When Designing .NET Applications

Proper design is a major factor that contributes to the scalability and performance of any .NET application.

Abstract

In today's software development world we are solving complex business problems and developing large business applications. Hence, proper design is a major factor that contributes to the scalability and performance of any .NET 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 better system performance.

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 db connections, streams and so on.
  • .NET provides proper structured blocks that can be used in your code to enforce the cleanup if the 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 then a process r app domain there is significant impact on performance of the application.

  • Cross application domain. 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.



  • 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 purposes.



  • 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.


Single Large Assemblies or Multiple Smaller Assemblies

In .NET an assembly is the unit of deployment; an application exists in the form of an assembly only. Any application you have built after compilation produces an assembly only. 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 various functionalities, classes and interfaces and so on should be part of one single chunky assembly or should be divided across multiple smaller assemblies.

The following are points to consider:

  • To help reduce your application's working set (in simple terms a 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 more depending on the count of assemblies.
    • Security checks are 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 improve 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 Resource worth Sharing

Many applications use threads internally and behind the scenes, that works well and doesn't cause much noise. The problem starts when we take control in ourselves 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.
    1. private void Page_Load(object sender, System.EventArgs e)  
    2. {  
    3.      if (Page.IsPostBack)  
    4.      {  
    5.           // Create and start a thread  
    6.           ThreadStart newTS = new ThreadStart(CalFunc);  
    7.           Thread th = new Thread(newTS);  
    8.            newTS.Start();  
    9.            ......  

  • 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.
    1. WaitCallback methodTarget = new WaitCallback( myClass.UpdateCache );  
    2. 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.