Posts /

Few thoughts about AOP with regards to ASP.NET Core and TypeScript

26 Feb 2017

The larger enterprise you are working for, the less business logic you are actually writing. It’s crosscutting concerns like logging, exception handling and resiliency in the face of other service’s failures that are bread and butter of programmers working with big codebases.

Aspect oriented programming turned our attention to those concerns, but the approach somehow failed to became a standard part of our toolkits. Ad hoc solutions to this kind of problems was developed: we don’t often create error-handling aspects when doing web development, but we sure do use extensibility points of ASP.NET WebApi / Core that allows us to customize exception-handling logic.

Why do Windsor interceptors or PostSharp aspects look so foreign? Is this something a programming language can help us with?

Controllers are not SRP-enough

We are writing controllers in the following way:

public class PlaceController : IController
{
    [Route("/Place/{placeId}")]
    Place GetPlace(PlaceId placeId) { ... }

    [Route("/Place/{coordinates}")]
    IList<Place> GetNearPlaces(GeoCoords coordinates) { ... }

    [Route("/Place/{humanReadableNameOfPointOfIntereset}")]
    IList<Place> GetNearPlaces(string humanReadableNameOfPointOfIntereset) { ... }

    [Route("/Place")]
    PlaceId PostPlace(Place place) { ... }
}

This corresponds to the REST paradigm. No one ever said though that implementation of the concept has to match to the concept exactly. The problem with having a controller with many methods is that we lack types for actions. We can’t wrap them inside a common decorator. Interceptors can be seen as a hack for this lack of method representation.

OOP way to tackle this would be give each action its own object:

public interface IAction<TReq, TRes>
  where TRes : IResponse
  where TReq : IRequest
{
    TRes ProcessAction(TReq req);
}

public class GetPlaceAction : IAction<PlaceId, Place>
{
    Place ProcessAction(PlaceId req)
    {

    }
}

Then, you could write decorator like this:

public class ExceptionLoggingDecorator<TReq, TRes> : IAction<TReq, TRes>
{
    private readonly IAction<TReq, TRes> _actualAction;

    public ExceptionLoggingDecorator(IAction<TReq, TRes> actualAction)
    {
        _actualAction = actualAction;
    }

    TRes ProcessAction(TReq req)
    {
        try
        {
            return _actualAction.ProcessAction(req);
        }
	    catch (Exception ex)
        {
            // process exception
        }
    }
}

… and register you actions using the following hypothetical API:

WebApi.Register(new ExceptionLoggingDecorator(new GetPlaceAction()));

When you represent your operations individually, it’s trivial to conclude you don’t need DynamicProxy-based ad-hoc-method-reification approaches.

This is not going to be another functional programming critique of OOP. I’m not trying to say that every object should have only one public method (and then it’s function with closure and so on…). I’m trying to say that organizing action method over shared resource they operate on is perhaps not the best axis. Maybe it’s a similar mistake as saving controllers inside of Controller folder instead of grouping your code on a per-feature basis.

Classes that are just lists of methods would use TypeScript’s Mapped Types

When talking about anemic domain models, people fixate on classes that contains only getters and setters and no domain logic. The other side of this problem are places that contain all the logic and no state, like repository layer – or the controller layer as we just noticed.

People like Repository and Controller methods-lists classes because namespaces are too weak in the current mainstream OOP languages. They are definitelly not a first class concept. When we want to group actions together for greater discoverability through IntelliSense, we reach for the only at-least-from-distance-reasonable tool we have: class.

What are we gravely missing there to make classes usable for those kind of usages is something like TypeScript mapped types mechanism.

Let’s say we want to build aspect/decorator over repository layer that would translate exceptions into the Result pattern. If you don’t want to use the approach from above and make every query a class, you will run into a problem: the resulting interface is different, because every return type is now not T, but Result<T>.

On the other end, at the Controller output, you may want to translate your internal error-propagating mechanism from Result<T> into Response<T> that would be able to tell the user a proper story about the error, a story that would not contain internal details.

If what you have is not a domain object, but just a list of methods, you need a way to work with them in the way Mapped Types provides. Notice that this problem doesn’t happen in good examples of OOP design. List<T> has many methods, but they work in various useful ways with the state the class encapsulate. In a good domain design, it’s difficult to find rationale for “let’s change every return type for something”.

Mapped Types can help us on the other side of anemic designs too, on the side of DTO-like classes without any behavior of their own. Anemic configuration object, for example, can be subject to following interface:

public interface IServiceConfiguration
{
    bool TurnedOn { get; set; }
    
    int MaximumNumberOfSimultaneousRequests { get; set; }
    
    // ...
}

This works great! It’s definitely much better than the common weakly-typed approach asking for hard-coded string key from a service locator.

There is an issue though. If a configuration value is not there, or if it cannot parsed, you want to represent the absence on the output. This can happen at the output of the method responsible for parsing. You would like to use Mapped Types for creating this by compiler:

public interface INullableServiceConfiguration
{
    Nullable<bool> TurnedOn { get; set; }
    
    Nullable<int> MaximumNumberOfSimultaneousRequests { get; set; }
    
    // ...
}

Without Mapped Types, you can write it by hand. Then you risk drifting of original version that won’t throw compile-time warning.

Conclussion

The space is wide open for finding better ways of organizing our basic infrastructure. With some language innovation, we can make our aspect-oriented tools much better. Without it, we can think about reifying concepts we only “represented” as methods so far.


Lukáš Lánský (e-mail)
Return to the list of posts


Share: Twitter Facebook Google+