DEV Community

Dmitry
Dmitry

Posted on

Streamlining .NET Development with Practical Aspects

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.

Automating Property Change Notifications with the Notify Aspect

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)));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

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.

Streamlining Logging with the Log Aspect

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();
Enter fullscreen mode Exit fullscreen mode

Once initialized, the following three aspects become available:

Log Aspect

The Log aspect can be applied to individual methods:

public class Model
{
    [Log]
    public void DoSomething1()
    {
    }

    public void DoSomething2()
    {
    }
}
Enter fullscreen mode Exit fullscreen mode

Or to an entire class:

[Log]
public class Model
{
    public void DoSomething1()
    {
    }

    public void DoSomething2()
    {
    }
}
Enter fullscreen mode Exit fullscreen mode

When applied to a class, the Log aspect is effectively applied to all methods within the class.

What does this aspect do?

  1. Exception Handling:
    The Log aspect wraps method execution in a try-catch block. If an exception is thrown, it logs the exception details, including its parameters.

  2. Method Execution Logging:
    If the logger’s MinimumLevel is set to Information or lower, the aspect logs each method call (excluding property getters and setters).

  3. Detailed Property Access Logging:
    When the logger’s 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.

NoLog Aspect

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()
    {
    }
}
Enter fullscreen mode Exit fullscreen mode

The NoLog aspect disables logging for the specified method, even if the Log aspect is applied at the class level.

Measure Aspect

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()
    {
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Simplifying DependencyProperty Management with the Bindable Aspect

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:

  1. Declare the static DependencyProperty:
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(int), typeof(MyControl),
    new PropertyMetadata(0, new PropertyChangedCallback(OnValueChanged)));
Enter fullscreen mode Exit fullscreen mode
  1. Create a local property to access the value:
public int Value
{
    get => (int)GetValue(ValueProperty);
    set => SetValue(ValueProperty, value);
}
Enter fullscreen mode Exit fullscreen mode

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.

Making DependencyProperty Registration Cleaner

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);
Enter fullscreen mode Exit fullscreen mode

This makes the DependencyProperty registration cleaner and less error-prone, improving code readability.

Eliminating GetValue and SetValue with Bindable Aspect

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; }
Enter fullscreen mode Exit fullscreen mode

With the Bindable aspect, the boilerplate code is reduced, resulting in cleaner and more maintainable classes.

Default Behavior and Custom Names

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; }
Enter fullscreen mode Exit fullscreen mode

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.

Conclusion

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:

By incorporating these tools into your projects, you can reduce boilerplate code, improve maintainability, and make your codebase cleaner and more efficient.

Top comments (0)