Introduction
The article AOP Container introduced an AOP model that enables any IoC container to have AOP
capabilities by configuration. As examples, the model was applied to MEF, Unity,
and Windsor containers with a simple console application.
Since then, I've enhanced this model to work with Web applications. In this article, I discuss how the AOP container together with Unity container is used to add AOP
capabilities to an ASP.NET MVC/AJAX/REST Web application.
Background
There are quite a few IoC (Inversion of Control) containers out there, e.g., MEF, Unity, Windsor, etc. They encourage decoupled component development by providing
mechanisms to wire these components into a single integrated application.
IoC containers do not provide AOP capabilities by themselves. They resort to other tools to provide AOP capabilities. AOPContainer is a tool that can
be integrated with any IoC container to add aspects to objects by configuration. It is based on the Dynamic Decorator (see the articles
Dynamic Decorator Pattern and
Add Aspects to Object Using Dynamic Decorator).
To add AOP capabilities to your application, you define your aspect methods, configure them in the configuration file, and use an AOP Container customized for your
IoC container to create objects. Behind the scenes, the aspects are attached to the objects.
Example
In the following sections, UnityContainer and AOPUnityContainer are used in an ASP.NET MVC/AJAX/REST Web application. The same example
is used in the article Components, Aspects, and Dynamic Decorator for an MVC/AJAX/REST Application.
But this time, instead of using Dynamic Decorator, we use AOP Container, which is a configurable version of the Dynamic Decorator. The problem definition is stated as follows.
Problem Definition
Displays employees based on the selection of a department.
Choose Application Type
In this article, an ASP.NET MVC application is chosen to address this problem. For an ASP.NET MVC application, a response from a Web application normally refreshes the entire page.
For example, when a department is selected, the entire page is refreshed to display the employees associated with the selected department. It is more desirable that only part of the page,
the employee table, is refreshed when a department is selected. This can be achieved using AJAX.
AJAX is a set of client-side technologies within a Web application, which can be applied to different types of Web applications: ASP.NET, ASP.NET MVC, PHP, etc. For this example,
AJAX is applied to an ASP.NET MVC application. Also, we have a few choices in terms of implementing AJAX: native JavaScript, ASP.NET AJAX Framework, jQuery, etc.
For our example, we use jQuery. Last, we have to decide on how to serve the AJAX requests. For this example, a WCF REST service is used to serve AJAX requests.
Define Components
Assume there are two components, Employee
and Department
. For Employee
, there is a corresponding RepositoryEmployee
component to contain a collection of objects of Employee
. For Department
, there is a corresponding RepositoryDepartment
component
to contain a collection of objects of Department
. The code of these components is listed as follows.
public interface IEmployee
{
System.Int32? EmployeeID { get; set; }
System.String FirstName { get; set; }
System.String LastName { get; set; }
System.DateTime DateOfBirth { get; set; }
System.Int32? DepartmentID { get; set; }
System.String DetailsByLevel(int iLevel);
}
public interface INotifyPropertyChanged
{
event PropertyChangedEventHandler PropertyChanged;
}
[PartCreationPolicy(CreationPolicy.NonShared)]
[Export]
public class Employee : IEmployee, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#region Properties
public System.Int32? EmployeeID { get; set; }
public System.String FirstName { get; set; }
public System.String LastName { get; set; }
public System.DateTime DateOfBirth { get; set; }
public System.Int32? DepartmentID { get; set; }
#endregion
public Employee(
System.Int32? employeeid
, System.String firstname
, System.String lastname
, System.DateTime bDay
, System.Int32? departmentID
)
{
this.EmployeeID = employeeid;
this.FirstName = firstname;
this.LastName = lastname;
this.DateOfBirth = bDay;
this.DepartmentID = departmentID;
}
public Employee() { }
public System.String DetailsByLevel(int iLevel)
{
System.String s = "";
switch (iLevel)
{
case 1:
s = "Name:" + FirstName + " " + LastName + ".";
break;
case 2:
s = "Name: " + FirstName + " " + LastName +
", Birth Day: " + DateOfBirth.ToShortDateString() + ".";
break;
case 3:
s = "Name: " + FirstName + " " + LastName +
", Birth Day: " + DateOfBirth.ToShortDateString() +
", Department:" + DepartmentID.ToString() + ".";
break;
default:
break;
}
return s;
}
}
public interface IDepartment
{
System.Int32? DepartmentID { get; set; }
System.String Name { get; set; }
}
public class Department : IDepartment
{
#region Properties
public System.Int32? DepartmentID { get; set; }
public System.String Name { get; set; }
#endregion
public Department(
System.Int32? departmentid
, System.String name
)
{
this.DepartmentID = departmentid;
this.Name = name;
}
public Department() { }
}
public interface IRepository<T>
{
List<T> RepList { get; set; }
void GetAll();
}
public class RepositoryEmployee : IRepository<IEmployee>
{
private List<IEmployee> myList = null;
public List<IEmployee> RepList
{
get { return myList; }
set { myList = value; }
}
public RepositoryEmployee()
{
}
public void GetAll()
{
myList = new List<IEmployee> {
new Employee(1, "John", "Smith", new DateTime(1990, 4, 1), 1),
new Employee(2, "Gustavo", "Achong", new DateTime(1980, 8, 1), 1),
new Employee(3, "Maxwell", "Becker", new DateTime(1966, 12, 24), 2),
new Employee(4, "Catherine", "Johnston", new DateTime(1977, 4, 12), 2),
new Employee(5, "Payton", "Castellucio", new DateTime(1959, 4, 21), 3),
new Employee(6, "Pamela", "Lee", new DateTime(1978, 9, 16), 4) };
}
}
public class RepositoryDepartment : IRepository<IDepartment>
{
private List<IDepartment> myList = null;
public List<IDepartment> RepList
{
get { return myList; }
set { myList = value; }
}
public RepositoryDepartment()
{
}
public void GetAll()
{
myList = new List<IDepartment> { new Department(1, "Engineering"),
new Department(2, "Sales"),
new Department(3, "Marketing"),
new Department(4, "Executive") };
}
}
In this application, the data for employees and departments is hard-coded in two lists to simplify our discussion. In a real world application, this data
is normally persisted in a relational database. Then, you will need to create a data layer to retrieve it and put it in lists.
It is worth noting that the employee list is populated by the inserting order without any kind of sorting in place. It is difficult to anticipate what kinds of sorting
to support for this component at this time. In applications, an object of the component may need to sort by last name. Another may need to sort by birth day.
A third may not need to sort at all. So, it is better to defer the implementation of sorting until the component is used in an application. By designing the component
RepositoryEmployee
without concerning about sorting, the Principle "Design components to meet business requirements in a general way" is followed.
This way, the component is stable and closed.
Define Aspects
Aspects are cross-cutting concerns. For Dynamic Decorator, an aspect is a method that takes an AspectContext
type object as its first parameter and
an object[]
type object as its second parameter and returns void
.
You may design your aspects in a more general way to use them in various situations. You may also design your aspects for some particular situations. For example,
you can define your entering/exiting log aspects in a general way and put them in the class SysConcerns
as follows:
public class SysConcerns
{
static SysConcerns()
{
ConcernsContainer.runtimeAspects.Add(
"DynamicDecoratorAOP.SysConcerns.EnterLog",
new Decoration(SysConcerns.EnterLog, null));
ConcernsContainer.runtimeAspects.Add(
"DynamicDecoratorAOP.SysConcerns.ExitLog",
new Decoration(SysConcerns.ExitLog, null));
}
public static void EnterLog(AspectContext ctx, object[] parameters)
{
StackTrace st = new StackTrace(new StackFrame(4, true));
Console.Write(st.ToString());
IMethodCallMessage method = ctx.CallCtx;
string str = "Entering " + ctx.Target.GetType().ToString() +
"." + method.MethodName + "(";
int i = 0;
foreach (object o in method.Args)
{
if (i > 0)
str = str + ", ";
str = str + o.ToString();
}
str = str + ")";
Console.WriteLine(str);
Console.Out.Flush();
}
public static void ExitLog(AspectContext ctx, object[] parameters)
{
IMethodCallMessage method = ctx.CallCtx;
string str = "Exiting " + ctx.Target.GetType().ToString() +
"." + method.MethodName + "(";
int i = 0;
foreach (object o in method.Args)
{
if (i > 0)
str = str + ", ";
str = str + o.ToString();
}
str = str + ")";
Console.WriteLine(str);
Console.Out.Flush();
}
}
As you can see, these methods access Target
and CallCtx
in a general way and can be shared by various types of objects to write entering/exiting logs.
On the other hand, some aspects may need to access more specific information. For example, you want to attach a change notification functionality only
to an Employee
object when its property is set. The following code defines some specific aspects:
public class LocalConcerns
{
class EmployeeLastnameComparer : IComparer<IEmployee>
{
public int Compare(IEmployee e1, IEmployee e2)
{
return String.Compare(e1.LastName, e2.LastName);
}
}
static LocalConcerns()
{
ConcernsContainer.runtimeAspects.Add(
"HRMVCAjax.Models.LocalConcerns.NotifyChange",
new Decoration(LocalConcerns.NotifyChange, null));
ConcernsContainer.runtimeAspects.Add(
"HRMVCAjax.Models.LocalConcerns.SecurityCheck",
new Decoration(LocalConcerns.SecurityCheck, null));
ConcernsContainer.runtimeAspects.Add(
"HRMVCAjax.Models.LocalConcerns.SortEmployee",
new Decoration(LocalConcerns.SortEmployee, null));
}
public static void NotifyChange(AspectContext ctx, object[] parameters)
{
((Employee)ctx.Target).NotifyPropertyChanged(ctx.CallCtx.MethodName);
}
public static void SecurityCheck(AspectContext ctx, object[] parameters)
{
}
public static void SortEmployee(AspectContext ctx, object[] parameters)
{
object target = ctx.Target;
if (target.GetType().ToString() == "ThirdPartyHR.RepositoryEmployee")
{
List<IEmployee> emps = ((IRepository<IEmployee>)target).RepList;
IEnumerable<IEmployee> query = emps.OrderByDescending(emp => emp,
new EmployeeLastnameComparer()).ToList<IEmployee>();
((IRepository<IEmployee>)target).RepList = (List<IEmployee>)query;
}
}
}
In the above code, the NotifyChange
method can only be used by a target of Employee
while SortEmployee
can only be used
by a target of RepositoryEmployee
.
Note: You may already have noticed that there is a static constructor in each of the classes SysConcerns
and LocalConcerns
.
Inside the static classes, each of the aspect methods defined in the classes is used to create an instance of Decoration
which is then added
to a dictionary ConcernsContainer.runtimeAspects
. The definition of ConcernsContainer
is as follows.
public class ConcernsContainer
{
static public Dictionary<string, Decoration> runtimeAspects =
new Dictionary<string, Decoration>();
}
The purpose of this Dictionary
and the static constructors is to keep an inventory for all Decoration
objects based on the aspect methods defined
in the application and make them accessible by the corresponding method names. It makes possible to configure aspects in the application configuration file
by specifying the corresponding method names.
Configure Aspects
In the configuration file, you specify how aspects are associated with objects. Here are some examples to demonstrate how the aspects are configured:
<configuration>
<configSections>
<section name="DynamicDecoratorAspect"
type="DynamicDecoratorAOP.Configuration.DynamicDecoratorSection,
DynamicDecoratorAOP.Configuration" />
</configSections>
<appSettings>
<add key="applicationPath"
value="C:\AspectContainerUsingDynamicDecorator\HRMVCAjax\bin\" />
</appSettings>
<DynamicDecoratorAspect>
<objectTemplates>
<add name="1"
type="ThirdPartyHR.RepositoryDepartment"
interface="ThirdPartyHR.IRepository`1[ThirdPartyHR.IDepartment]"
methods="GetAll"
predecoration="SharedLib.SysConcerns.EnterLog,SharedLib.SysConcerns"
postdecoration="SharedLib.SysConcerns.ExitLog,SharedLib.SysConcerns" />
<add name="2"
type="ThirdPartyHR.RepositoryEmployee"
interface="ThirdPartyHR.IRepository`1[ThirdPartyHR.IEmployee]"
methods="GetAll"
predecoration=""
postdecoration="HRMVCAjax.Models.LocalConcerns.SortEmployee" />
<add name="3"
type="ThirdPartyHR.Department"
interface="ThirdPartyHR.IDepartment"
methods="DetailsByLevel,get_EmployeeID"
predecoration="SharedLib.SysConcerns.EnterLog,SharedLib.SysConcerns"
postdecoration="" />
</objectTemplates>
</DynamicDecoratorAspect>
</configuration>
First of all, you need to add a section <DynamicDecoratorAspect>
in your configuration file. You also need to add an applicationPath
key to <appSettings>
to specify the physical path of the Web application. It is required for a Web application since a Web application path is different
from the execution program path. Then, inside <objectTemplates>
of <DynamicDecoratorAspect>
, you add individual elements. For each element
inside <objectTemplates>
, the following attributes need to be specified:
type
- target typeinterface
- interface returnedmethods
- names of target methods which will be attached to the aspects specified by predecoration
and postdecoration
predecoration
- preprocessing aspectpostdecoration
- post-processing aspect
Notes:
- The names in the value of the
methods
attribute are comma separated. For example, "DetailsByLevel,get_EmployeeID"
. - The value of the
predecoration
attribute has two parts and is separated by a comma. The first part specifies the aspect name while
the second part specifies the assembly name in which the aspect is defined, for example, "SharedLib.SysConcerns.EnterLog,SharedLib.SysConcerns"
.
If the second part is not specified, it is assumed that the aspect is defined in the entry assembly, for example, "ConsoleUtil.LocalConcerns.SecurityCheck"
. - The value of the
postdecoration
attribute has two parts and is separated by a comma. The first part specifies the aspect name while the second part
specifies the assembly name in which the aspect is defined. If the second part is not specified, it is assumed that the aspect is defined in the entry assembly.
Change Global.asax.cs
We need to make a few application wide changes to use the aspects and the containers in our application. The code of Global.asax.cs is listed below:
public class MvcApplication : System.Web.HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add(new ServiceRoute("HRRestService",
new WebServiceHostFactory(), typeof(Services.HRRestService)));
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "DepEmployees",
action = "Index", id = UrlParameter.Optional }
);
}
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
FileStream fileStream = null;
string path = Path.GetDirectoryName(Server.MapPath("~"));
if (!File.Exists(path + "\\hrlog.txt"))
{
fileStream = new FileStream(path + "\\hrlog.txt", FileMode.Create);
}
else
fileStream = new FileStream(path + "\\hrlog.txt", FileMode.Truncate);
TextWriter tmp = Console.Out;
Application["origOut"] = tmp;
StreamWriter sw1 = new StreamWriter(fileStream);
Console.SetOut(sw1);
Application["logStream"] = sw1;
IUnityContainer container = null;
container = new UnityContainer();
((IUnityContainer)container).RegisterType<IEmployee, Employee>(
new InjectionConstructor());
((IUnityContainer)container).RegisterType<IRepository<IDepartment>,
RepositoryDepartment>(new InjectionConstructor());
((IUnityContainer)container).RegisterType<IRepository<IEmployee>,
RepositoryEmployee>(new InjectionConstructor());
Containers.iocContainer = container;
Containers.aopContainer = new AOPUnityContainer(container);
}
protected void Application_End(object sender, EventArgs e)
{
TextWriter origStrm = (TextWriter)Application["origOut"];
Console.SetOut(origStrm);
StreamWriter tmp = (StreamWriter)Application["logStream"];
Stream fileStream = tmp.BaseStream;
tmp.Close();
fileStream.Close();
IDisposable container = Containers.iocContainer;
container.Dispose();
}
}
In the above code, Console
's out is redirected to a file stream. It is required since the log aspect methods use Console
to write logs in a Web application. Then, a UnityContainer
is created and a few types are registered to it. An AOPUnityContainer
is also created by passing in the instance of the UnityContainer
. The UnityContainer
object and the AOPUnityContainer
object are assigned to static variables Containers.iocContainer
and Containers.aopContainer
, respectively, for later use in the application.
The definition of Containers
is as follows:
public class Containers
{
static public IUnityContainer iocContainer = null;
static public AOPContainer aopContainer = null;
}
Use Containers
The controller code is listed below:
public class DepEmployeesController : Controller
{
public DepEmployeesController()
{
}
public ActionResult Index()
{
IRepository<IEmployee> rpEmployee = null;
IRepository<IDepartment> rpDepartment = null;
rpDepartment = Containers.aopContainer.Resolve<RepositoryDepartment,
IRepository<IDepartment>>();
rpDepartment.GetAll();
rpEmployee = Containers.aopContainer.Resolve<RepositoryEmployee,
IRepository<IEmployee>>();
rpEmployee = ObjectProxyFactory.CreateProxy<IRepository<IEmployee>>(
rpEmployee,
new String[] { "GetAll" },
new Decoration(new DecorationDelegate(SysConcerns.EnterLog), null),
new Decoration(new DecorationDelegate(SysConcerns.ExitLog), null));
rpEmployee.GetAll();
List<SelectListItem> depList = new List<SelectListItem>();
SelectListItem sli = null;
sli = new SelectListItem();
sli.Value = "";
sli.Text = "";
depList.Add(sli);
foreach (IDepartment d in rpDepartment.RepList)
{
sli = new SelectListItem();
sli.Value = d.DepartmentID.Value.ToString();
sli.Text = d.Name;
depList.Add(sli);
}
ViewData["depSel"] = new SelectList(depList, "Value", "Text");
return View(rpEmployee.RepList);
}
}
As you see in the code, both rpDepartment
and rpEmployee
are resolved using Containers.aopContainer
. Therefore, they have been
attached the aspects configured in the configuration file. For rpDepartment
, its GetAll
has been attached a preprocessing aspect
SharedLib.SysConcerns.EnterLog
and a postprocessing aspect SharedLib.SysConcerns.ExitLog
. For rpEmployee
, its GetAll
has been
attached a postprocessing HRMVCAjax.Models.LocalConcerns.SortEmployee
aspect.
Note that the rpEmployee
coming out of the AOPContainer
's Resolve
method has only the HRMVCAjax.Models.LocalConcerns.SortEmployee
aspect attached as configured in the configuration file. What if you want to add enter/exit logging aspects to it? You can achieve this by calling ObjectProxyFactory.CreateProxy
as shown in the above code. Now, rpEmployee
has not only the sorting capability but also the enter/exit logging capabilities. It is very flexible to use both
configuration and code to add aspects to objects.
The REST service code is listed as follows. It accepts a department ID and returns a list of employees to the client.
[ServiceContract]
[AspNetCompatibilityRequirements(RequirementsMode =
AspNetCompatibilityRequirementsMode.Allowed)]
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
public class HRRestService
{
private static int iStaticDep = 0;
[WebGet(UriTemplate = "?dep={dep}")]
public List<Employee> EmployeesByDepartment(string dep)
{
IRepository<IEmployee> rpEmployee = null;
IRepository<IDepartment> rpDepartment = null;
rpDepartment = Containers.iocContainer.Resolve<RepositoryDepartment>(); ;
rpDepartment.GetAll();
rpEmployee = Containers.aopContainer.Resolve<RepositoryEmployee,
IRepository<IEmployee>>();
rpEmployee.GetAll();
IDepartment dpSel = rpDepartment.RepList[Convert.ToInt16(dep) - 1];
dpSel = ObjectProxyFactory.CreateProxy<IDepartment>(
dpSel,
new String[] { "get_DepartmentID" },
new Decoration(new DecorationDelegate(SysConcerns.EnterLog), null),
null);
iStaticDep = dpSel.DepartmentID.Value;
List<IEmployee> empSel = null;
if (rpEmployee.RepList != null)
{
empSel = rpEmployee.RepList.FindAll(
(IEmployee emp) => { return emp.DepartmentID.Value == iStaticDep; });
}
List<Employee> empObj = new List<Employee>();
foreach (IEmployee i in empSel)
{
empObj.Add((Employee)i);
}
return empObj;
}
}
Note that rpDepartment
is resolved directly from the IUnityContainer
's Resolve
method. Therefore, there is no aspect
attached it, which means it is used as is without additional functionality added. On the other hand, rpEmployee
is resolved from AOPContainer
's,
Resolve
. Therefore, it has the sorting capability configured in the configuration file.
Last, ObjectProxyFactory.CreateProxy
is called for a selected Department to add enter logging to its get_DepartmentID
method.
Finally, the view code is as follows.
<%@ Page Title="" Language="C#"
MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<dynamic>" %>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<% Html.BeginForm("Index", "DepEmployees"); %>
<span>Department</span><br />
<%= Html.DropDownList("depSel")%>
<br /> <br />
<span>Employees</span><br />
<div id="grid">
<table cellspacing="0" rules="all" border="1"
id="GridView1" style="border-collapse:collapse;">
<tr>
<th>EmployeeID</th><th>FirstName</th><th>LastName</th>
<th>DateOfBirth</th><th>DepartmentID</th>
</tr>
<% foreach (var itm in Model) { %>
<tr>
<td><%= itm.EmployeeID %></td><td><%= itm.FirstName%></td>
<td><%= itm.LastName%></td><td><%= itm.DateOfBirth%></td>
<td><%= itm.DepartmentID%></td>
</tr>
<% } %>
</table>
</div>
<% Html.EndForm(); %>
<script type='text/javascript'>
$(document).ready(function () {
$("select").change(function () {
$.get("HRRestService?dep=" + $(this).val(), function (data) {
$("#grid").html(
'<table cellspacing="0" rules="all" border="1"
id="emp" style="border-collapse:collapse;">' +
'<tr>' +
'<th>EmployeeID</th><th>FirstName</th><th>LastName</th>
<th>DateOfBirth</th><th>DepartmentID</th>' +
'</tr>');
$(data).find("Employee").each(function() {
$("#emp").append(
'<tr>' +
'<td>' + $(this).find("EmployeeID").text() + '</td><td>' +
$(this).find("FirstName").text() + '</td><td>' +
$(this).find("LastName").text() + '</td><td>' +
$(this).find("DateOfBirth").text() + '</td><td>' +
$(this).find("DepartmentID").text() + '</td>' +
'</tr>'
);
});
$("#grid").append('</table>');
});
});
})
</script>
</asp:Content>
Pay attention to the above JavaScript code written in jQuery. When a department is selected, an AJAX request is sent to the service HRRestService
with the selected
department ID as query string. The service returns an XML data file which contains a list of employees associated with the selected department. The callback function parses
the XML data and uses that to update the employee table in the page.
When a browser accesses the application the first time, the MVC controller creates both the department list and the employee table, and sends the response back to the client.
When a department is selected, the jQuery code makes an AJAX request to the REST service with the selected department ID. Once the service responds back, the callback function
refreshes the employee table to display the employees associated with the selected department.
When it runs, all employees are displayed as follows:
When a department is selected, the employees for that department are displayed.
Quit the browser session, stop the application, and open the file hrlog.txt. You will see the following logs:
at HRMVCAjax.Controllers.DepEmployeesController.Index()
in C:\AopContainerMvcAjax\HRMVCAjax\Controllers\DepEmployeesController.cs:line 45
Entering ThirdPartyHR.RepositoryEmployee.GetAll()
Exiting ThirdPartyHR.RepositoryEmployee.GetAll()
at HRMVCAjax.Services.HRRestService.EmployeesByDepartment(String dep)
in C:\AopContainerMvcAjax\HRMVCAjax\Services\HRRestService.cs:line 48
Entering ThirdPartyHR.Department.get_DepartmentID()
Points of Interest
AOP Container combined with IoC container makes adding AOP capabilities to a Web application very easy. You define your aspect methods and configure them in the configuration file.
If you need an object with aspects, you use the AOP container to create it. If you need an object without aspects, just create it using the IoC container.
You can also mix adding aspects using configuration and adding aspects using code. This way, you can use aspect configuration for general purpose while using code to chain more
aspects to meet your specific requirements as needed.
Although this article discusses AOP container with Unity IoC container for ASP.NET MVC applications, you should be able to easily combine AOP Container with other IoC containers
like MEF and Windsor for other application types like ASP.NET, Silverlight, WinForms, etc.