Aspect-oriented programming (AOP) provides a robust approach to encapsulate cross-cutting concerns into reusable components called aspects. By separating these concerns from business logic, AOP helps streamline development, reduce boilerplate code, and enhance maintainability. In this article, I’ll explore three practical aspects that I am using for almost all my projects: Notify, Log, and Bindable, demonstrating how they simplify common programming tasks and improve code quality.
All examples are implemented using the Aspect Injector, but the same logic can be adapted to other AOP frameworks. This approach is not tied to any specific library and can be easily customized to fit your project’s needs.
When working with the MVVM pattern (and not only), there’s a frequent need to track changes in properties. To achieve this, the model class for which we want to track property changes must implement the INotifyPropertyChanged
interface. Each property then has to explicitly invoke the PropertyChangedEventHandler
when modified:
public class Model : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged = delegate { };
private string m_text;
public string Text
{
get
{
return m_text;
}
set
{
m_text = value;
PropertyChanged(this, new PropertyChangedEventArgs(nameof(Text)));
}
}
}
This approach bloats the code and requires inserting repetitive constructs. Even when simplified, such as avoiding plain text arguments, it still results in verbose code, increasing the chance of missing something or introducing errors.
This problem is perfectly solved using aspects. Fortunately, the Aspect Injector framework provides a built-in Notify
aspect for automating property change notifications. With this, the above code can be rewritten as follows:
public class Model
{
[Notify]
public string Text { get; set; }
}
The aspect will automatically generate a PropertyChangedEventHandler
and invoke it on behalf of the class. This works seamlessly with WPF UI.
But what if we want to monitor property changes for custom purposes? For example, we might need to attach to the PropertyChanged
event and execute additional logic when properties change. Unfortunately, relying solely on the Notify aspect provided by Aspect Injector won’t work as expected:
public class Model : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged = delegate { };
public Model()
{
this.PropertyChanged += OnPropertyChanged;
}
private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Text))
{
// This block will never be executed...
}
}
[Notify]
public string Text { get; set; }
}
To enable this, we need to modify the implementation of the aspect. Specifically, the aspect should detect whether the class implements INotifyPropertyChanged
and invoke the internal event. This can be achieved using the power of reflection:
public static bool FirePropertyChanged(string propertyName, INotifyPropertyChanged obj)
{
var eventDelegate = GetPropertyChangedField(obj.GetType())?.GetValue(obj) as MulticastDelegate;
if (eventDelegate == null)
return false;
var delegates = eventDelegate.GetInvocationList();
foreach (var dlg in delegates)
try
{
dlg.Method.Invoke(dlg.Target, new object[] { obj, new PropertyChangedEventArgs(propertyName) });
}
catch (TargetInvocationException targetInvocationException)
{
if (targetInvocationException.InnerException != null)
throw targetInvocationException.InnerException;
}
return true;
}
private static FieldInfo? GetPropertyChangedField(Type objType)
{
while (true)
{
var property = objType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic)
.SingleOrDefault(x => x.FieldType == typeof(PropertyChangedEventHandler));
if (property != null)
return property;
if (objType.BaseType?.GetInterface(nameof(INotifyPropertyChanged)) == null)
return null;
objType = objType.BaseType;
}
}
A detailed implementation of this aspect can be found in the OutWit repository on GitHub.
To use this extended functionality, you need to install the OutWit.Common.Aspects
NuGet package and utilize the Notify
aspect from it. With this modification, the code will behave as expected:
public class Model : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged = delegate { };
public Model()
{
this.PropertyChanged += OnPropertyChanged;
}
private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Text))
{
Trace.Write("I am here!");
}
}
[Notify]
public string Text { get; set; }
}
This extended Notify
aspect ensures both WPF compatibility and the ability to react to property changes for custom purposes, making it a robust solution for property change tracking.
Maintaining logs is an essential practice for any sufficiently complex application, as it greatly simplifies debugging and support. Logs help developers understand the sequence of events or user actions that led to a specific result.
For larger projects, explicitly calling logging methods every time they’re needed can be tedious and error-prone. The OutWit.Common.Logging
package provides a powerful set of logging aspects that can automate much of this process. The source code for these aspects is available here.
This implementation is based on Serilog but can be easily adapted to other logging frameworks. Before using these aspects, you must initialize the logger:
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Is(LogEventLevel.Information)
.Enrich.WithExceptionDetails()
.WriteTo.File(@"D:\Log\Log.txt",
rollingInterval: RollingInterval.Day,
rollOnFileSizeLimit: true,
fileSizeLimitBytes: 524288)
.CreateLogger();
Once initialized, the following three aspects become available:
The Log
aspect can be applied to individual methods:
public class Model
{
[Log]
public void DoSomething1()
{
}
public void DoSomething2()
{
}
}
Or to an entire class:
[Log]
public class Model
{
public void DoSomething1()
{
}
public void DoSomething2()
{
}
}
When applied to a class, the Log aspect is effectively applied to all methods within the class.
What does this aspect do?
try-catch
block. If an exception is thrown, it logs the exception details, including its parameters.MinimumLevel
is set to Information
or lower, the aspect logs each method call (excluding property getters and setters).MinimumLevel
is set to Verbose
, the aspect also logs access to properties.
This allows you to control the level of detail in your logs by simply adjusting the logger’s configuration.
If the Log
aspect is applied to an entire class, but you need to exclude specific methods from logging (e.g., frequently called methods that could clutter the logs), the NoLog
aspect can be used:
[Log]
public class Model
{
public void DoSomething1()
{
}
[NoLog]
public void DoSomething2()
{
}
}
The NoLog
aspect disables logging for the specified method, even if the Log
aspect is applied at the class level.
Sometimes, the primary goal is to measure how long an operation takes to execute. Applying the Measure
aspect to a method will log its execution time in milliseconds:
public class Model
{
public void DoSomething1()
{
}
[Measure]
public void DoSomething2()
{
}
}
When DoSomething2
is called, the log will include a message indicating how long the method took to execute.
These aspects not only simplify logging but also ensure consistency across your application. Whether you need basic logging, selective exclusions, or precise performance measurements, these tools provide a flexible and powerful solution for your .NET applications.
When working with WPF, dealing with DependencyProperty
is a frequent and often tedious task. Declaring a DependencyProperty
correctly is even more cumbersome than handling INotifyPropertyChanged
.
For every DependencyProperty
, you typically need to:
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(int), typeof(MyControl),
new PropertyMetadata(0, new PropertyChangedCallback(OnValueChanged)));
public int Value
{
get => (int)GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
This process is verbose and error-prone, with multiple places where mistakes can be introduced or something might be forgotten.
To simplify this workflow, the OutWit.Common.MVVM
package provides a set of tools, including the Bindable aspect. The source code is available here.
The BindingUtils
utility included in the package simplifies the declaration of DependencyProperty
by offering a more concise syntax:
public static readonly DependencyProperty ValueProperty =
BindingUtils.Register<MyControl, int>(nameof(Value), OnValueChanged);
This makes the DependencyProperty
registration cleaner and less error-prone, improving code readability.
The Bindable
aspect takes simplification even further by eliminating the need for explicit GetValue
and SetValue
calls in your property definition. Here’s how it looks in action:
[Bindable]
public int Value { get; set; }
With the Bindable
aspect, the boilerplate code is reduced, resulting in cleaner and more maintainable classes.
By default, the aspect assumes that the DependencyProperty
corresponding to a property named [Name]
is declared as [Name]Property
. For example, for the property Value
, the aspect expects the DependencyProperty
to be named ValueProperty
.
If your DependencyProperty
uses a different name, you can specify it explicitly as a parameter to the attribute:
[Bindable("CustomDependencyProperty")]
public int Value { get; set; }
This allows flexibility while still maintaining a concise and readable property definition.
The Bindable
aspect, combined with BindingUtils
, significantly reduces the amount of repetitive code required when working with DependencyProperty
. It ensures consistency and improves readability, helping developers focus more on the logic of their application rather than boilerplate code.
Aspects are a powerful tool to simplify repetitive and error-prone coding tasks, allowing developers to focus on the core logic of their applications. Here, I explored three practical aspects—Notify
, Log
, and Bindable
—and demonstrated how they can streamline property change notifications, logging, and DependencyProperty
management in .NET development.
All examples presented here leverage the following packages:
OutWit.Common.Aspects
: Source CodeOutWit.Common.Logging
: Source CodeOutWit.Common.MVVM
: Source Code
By incorporating these tools into your projects, you can reduce boilerplate code, improve maintainability, and make your codebase cleaner and more efficient.