Click here to Skip to main content
15,887,881 members
Articles / Programming Languages / C#

Dynamic Service Result Builder (Exception Pattern, Result Pattern, Decorator Pattern)

Rate me:
Please Sign up or sign in to vote.
3.67/5 (7 votes)
14 Oct 2020CPOL14 min read 7.3K   3  
What type of result should the Service method return to the Caller
I answer some questions that cross our minds when we start designing our Application. What type will we return when calling Methods of Services? What properties will it contain? Will the expected result of this method be sufficient? In this topic, I attempted to answer these questions by offering some scenarios about how services methods would connect to the ASP.NET Core 3.1 Controllers.

Table of Contents

We will learn things we did not know or increase our understanding of things we know.

Introduction

What type of result should the service method return to the caller?

  1. What type will we return when calling Methods of Services?
  2. What properties will it contain?
  3. Will the expected result of this method be sufficient?

These questions are always in our minds when we start designing our special services for our program. In this topic, we will try to talk about some scenarios about how the Methods of Services communicate with the ASP.NET Core 3.1 Controller.

At first, you will think that this topic is special, but after you finish reading it, you will find that you can apply it in any scenario in which you need to return a meaningful result to the Caller.

We used to return one result for service methods, which is the possible result of that function (like Boolean, Integer, Models, etc.).

Notice

But before we start, we expect that you have sufficient experience or familiarity with ASP.NET Core and some basic object-oriented programming (such as Inheritance, Polymorphism, Interfaces, Generics, etc.) and you will find that we have written the code in C# 8.0. You can read more about C# 8.0 here.

You will find that in some classes we have removed the unnecessary codes for this topic, but at the bottom of the topic, you will find a link to download all the programs that we have created.

Using the Code

Here is an illustrative example of a service:

C#
public class xxxService : Service, IxxxService
{
    /// <summary>
    /// Return model
    /// </summary>
    public IModel GetByID(Guid id)
    {
        return new xxxModel() { … };
    }    /// <summary>
    /// Return Integer
    /// </summary>
    public int GetLastIndex()
    {
        return 0;
    }    /// <summary>
    /// Return Boolean
    /// </summary>
    public bool Exists(IModel model)
    {
       return true;
    }
}

But in some Methods of Services, we find ourselves that we want to tell the Caller some additional information about why he got such a result, for example, if we want to tell him about the error that happened and why this error occurred, the type of returned data will not be sufficient to add the other explanatory information.

C#
public class xxxService : Service, IxxxService
{
    /// <summary>
    /// Return model
    /// </summary>
    public IModel GetByID(Guid id)
    {
        try{
            return new xxxModel() { … };
        }catch (Exception){
            // Is this result sufficient?
            return null;
        }
    }    /// <summary>
    /// Return Integer
    /// </summary>
    public int GetLastIndex()
    {
        try{
            return 1;
        }catch (Exception){
            // Is this result sufficient?
            return -1;
        }
    }
  
    /// <summary>
    /// Return Boolean
    /// </summary>
    public bool? Exists(IModel model)
    {
        try{
            return true;
        }catch (Exception){
            // Is this result sufficient?
            return null;
        }
    }
}

We will use the most famous examples in this topic which is the method used by programs to log in.

Service Base Class

C#
public interface IService
{
    TDTO Map<TEntity, TDTO>(TEntity entity)
        where TEntity : class, IEntity, new()
        where TDTO : class, IDTO, new();

    TEntity Map<TEntity, TDTO>(TDTO dto)
        where TEntity : class, IEntity, new()
        where TDTO : class, IDTO, new();
}

// This is the base class for all services in our projects
public abstract class Service : IService
{
    protected Service(IMapper mapper) => Mapper = mapper;

    protected IMapper Mapper { get; set; }

    public virtual TDTO Map<TEntity, TDTO>(TEntity entity)
        where TEntity : class, IEntity, new()
        where TDTO : class, IDTO, new()
        => Mapper.Map<TDTO>(entity);

    public virtual TEntity Map<TEntity, TDTO>(TDTO dto)
        where TEntity : class, IEntity, new()
        where TDTO : class, IDTO, new()
        => Mapper.Map<TEntity>(dto);
}

Login Service

C#
public class LoginService : Service, ILoginService
{
    public LoginService(IMapper mapper) : base(mapper) { }

    public LoginDto Authenticate(string userName, string pw)
    {
        try
        {
            // This is just an example of the return type so we haven't
            // listed all the code this function needs to fetch data
            // from the database and the validation code.

            return userName == "user" && pw == "pw" ? Map<Login, LoginDto>(new Login() 
                                { UserName = "default user" }) : null;
        }
        catch
        {
            // Is this result sufficient?
            return null;
        }
    }
}

The Controller Class

C#
[ApiController]
[Route("[controller]")]
public class LoginController : ControllerBase
{
    private readonly ILoginService _service;

    public LoginController(ILoginService service) => _service = service;

    [AllowAnonymous]
    [HttpPost]
    public IActionResult Post([FromBody] LoginModel model)
    {
        var dto = _service.Authenticate(model.UserName, model.Password);

        return dto is null
            ? (IActionResult)BadRequest("The username or password you entered is incorrect.")
            : Ok(dto);
    }
}

But sometimes, the username and password are correct, but the cause of the error is the lack of a connection with the database or the presence of any other unexpected error, so we can say here that the returned result is not sufficient to explain the reasons for the failure.

How can we tell the Caller this, so that he, in turn, explains the error to the user?

The answer to this question is the focus of our topic for today.

We will include some scenarios that will help us return enough information to clarify Methods of Services and try to explain the programming procedures in a simple way.

We will not go into this post about other classes design methods, that such a program needs. And about the methods used in its design such as (Repository Pattern, Unit of Work Patterns, Services in Domain-Driven, Design ... etc.), but we will only talk about the result data types that the Methods of Services will return to the Caller.

Scenario 1 - Throw Exceptions or Exception Pattern

We will first start with one of the scenarios that do not require a large cost of time to apply to the services that we previously created, as it only needs Throw Exceptions so that the Caller can build more accurate results, to inform the user about the reasons for the failure of this process.

Why do we need to create custom exceptions?

There are no specific answers that benefit this purpose, some see it as an overburden and the other finds it useful, from my experience, it depends on how much you have to do once you discover the exception, for example, if you only want to throw this exception then there is no need to create them, but if you want handling the exception and sending it back to the Caller, with new exception more accurate, the Caller can recognize and deal with it more accurately, so here you have to create them.

  • If we do not find any of the pre-existing exception classes, we can specify the exception that we expect to occur.
  • To do some complex work depending on what the exception is and how it will be thrown.
  • Hierarchy of exceptions. To help make it easier to catch errors, we can catch a set of exceptions by capturing the Base class:
C#
try
{
    // ...
}
// Here we can catch all exceptions inherited from ServiceException
catch(ServiceException ex) 
{
    // ...
}
  • You do not have a clash.
  • Provide meaningful messages.

And now, we can throw the exceptions for our project that we created previously so that the Caller can collect some additional information about the workflow of this function and what exceptions it encountered.

Base Classes for All Our Exceptions

C#
/// <summary>
/// Base class for all applications
/// </summary>
public class ServiceException : Exception
{
    public ServiceException() { }
    public ServiceException(string message) : base(message) { }
    public ServiceException(string message, Exception innerException) : 
                            base(message, innerException) { }
}

/// <summary>
/// Base class for ASP.NET WEB API applications
/// </summary>
public class HttpStatusCodeException : ServiceException
{
    public HttpStatusCodeException() { }
    public HttpStatusCodeException(string message) : base(message) { }
    public HttpStatusCodeException(string message, 
           HttpStatusCode httpStatusCode) : this(message, null, httpStatusCode) { }
    public HttpStatusCodeException(string message, 
           Exception innerException) : this(message, innerException, 
           HttpStatusCode.InternalServerError) { }
    public HttpStatusCodeException(string message, 
           Exception innerException, HttpStatusCode httpStatusCode) : 
           base(message, innerException) => HttpStatusCode = httpStatusCode;

    public HttpStatusCode HttpStatusCode { get; }
}

At first, we will notice that the HttpStatusCodeException is sufficient to proceed in the program, but if we look closely, we will notice that we need to pass the HttpStatusCode every time we need to create an instance from this class, and we must every time test this property while catch this exception. To make this easier, we will create some special classes from this base class to have more precise classes and to maintain consistency of the program so that there is no inconsistency in understanding the exceptions catching plan.

All Other Exceptions Classes

C#
public class BadRequestException : HttpStatusCodeException
{
    public BadRequestException() { }
    public BadRequestException(string message) : base(message, HttpStatusCode.BadRequest) { }
    public BadRequestException(string message, Exception innerException) : 
                               base(message, innerException) { }
}

public class EntityNotFoundException : HttpStatusCodeException
{
    public EntityNotFoundException() { }
    public EntityNotFoundException(string message) : base(message) { }
    public EntityNotFoundException(string message, Exception innerException) : 
                                   base(message, innerException) { }
    public EntityNotFoundException(Type entityType) : 
    base($"The {entityType?.Name ?? "entity"} does not exist.", HttpStatusCode.NotFound) { }
    public EntityNotFoundException(Type entityType, Exception innerException)
        : base($"The {entityType?.Name ?? "entity"} does not exist.", 
               innerException, HttpStatusCode.NotFound) { }
}

public class ForbiddenException : HttpStatusCodeException
{
    public ForbiddenException() { }
    public ForbiddenException(string message) : base(message, HttpStatusCode.Forbidden) { }
    public ForbiddenException(string message, Exception innerException) : 
                              base(message, innerException) { }
}

public class InternalServerErrorException : HttpStatusCodeException
{
    public InternalServerErrorException() { }
    public InternalServerErrorException(string message) : 
           base(message, HttpStatusCode.InternalServerError) { }
    public InternalServerErrorException(string message, Exception innerException) : 
           base(message, innerException) { }
}

public class MethodNotAllowedException : HttpStatusCodeException
{
    public MethodNotAllowedException() { }
    public MethodNotAllowedException(string message) : 
           base(message, HttpStatusCode.MethodNotAllowed) { }
    public MethodNotAllowedException(string message, Exception innerException) : 
           base(message, innerException) { }
}

public class UnprocessableEntityException : HttpStatusCodeException
{
    public UnprocessableEntityException() { }
    public UnprocessableEntityException(string message) : 
           base(message, HttpStatusCode.UnprocessableEntity) { }
    public UnprocessableEntityException(string message, Exception innerException) : 
           base(message, innerException) { }
}

// This is a special class that we will use 
// when we want to add a warning to the ServiceResult,
// When we implement the Result Pattern
public class WarningException : HttpStatusCodeException
{
    public WarningException(object resul, string message) : 
           base(message, HttpStatusCode.OK) => Result = resul;

    public object Result { get; }
}

When starting to implement this service, we will follow the usual method to return the expected value, but taking into account what we have talked about earlier to implement this pattern. For example, when the required user is not found, we must throw an exception instead of returning the value of null, and so on whenever we want to send additional information to the Caller, we will throw an exception, and we will reformulate the unexpected exceptions, with new significant exceptions in our program, to allow us when the errors start to discover a more clear formulation of a new exception, and so on until we see that we have covered all the unexpected exceptions in our program.

Login Service

C#
public interface ILoginService : IService
{
    LoginDto Authenticate(string userName, string pw);
}

public class LoginService : Service, ILoginService
{
    public LoginService(IMapper mapper) : base(mapper) { }

    public LoginDto Authenticate(string userName, string pw)
    {
        try
        {
            // This is just an example of the return type so we haven't
            // listed all the code this function needs to fetch data
            // from the database and the validation code.

            // ===== User with Warning =====
            if (userName == "user_a" && pw == "pw")
            {
                // The ASP.NET Core will treat this warning as an Internal Server Error.
                throw new WarningException(
                       Map<Login, LoginDto>(new Login() { UserName = "Warning user" }),
                       "For more than ten months you have not changed your password, 
                        we recommend that you change it as soon as possible.");
            }

            // ===== Valid user =====
            if (userName == "user_b" && pw == "pw")
            {
                return Map<Login, LoginDto>(new Login() { UserName = "Valid user" });
            }

            // ===== User with Exception =====
            if (userName == "user_c" && pw == "pw")
            {
                throw new ForbiddenException($"User {userName} is currently blocked.");
            }

            throw new BadRequestException
                  ("The username or password you entered is incorrect.");
        }
        catch (Exception ex)
        {
            // Probably one of the exceptions we designed, so we test the exception first.
            throw ex as HttpStatusCodeException ?? 
                  new InternalServerErrorException(ex.Message, ex);

            /*
             * ====================================================
             * During the troubleshooting process, we can throw other types 
             * when analyzing this Exception.
             * ====================================================
             * if (ex is xxxException) {
             *      throw new A_Custme_Exception(ex.Message, ex);
             * }
             * ...
             */
        }
    }
}

And in our controller, we must catch the exceptions that will be thrown, and gather as much information as possible to show clear results to the user:

C#
public class LoginController : AppController<ILoginService>
{
    public LoginController(ILoginService service) : base(service) { }

    [AllowAnonymous]
    [HttpPost]
    public IActionResult Authenticate([FromBody] LoginModel model)
    {
        try
        {
            var dto = Service.Authenticate(model.UserName, model.Password);

            /*
             * In this case, we should not test if the result is null,
             * because there is a prior agreement between the service and
             * the caller to catch bugs, if he wants more details about the exceptions.
             */

            return Ok(dto);
        }
        catch (Exception ex)
        {
            return GetResult(ex);
        }
    }
}

Advantages

Special exceptions are one of the means to understand how the program works and what exceptions are expected to occur, but we cannot list all exceptions, there are exceptions that we cannot expect so all programs are subject to a test driven design and a manual test plan simultaneously.

Program stability is one of the main advantages of the exceptions.

Disadvantages

It requires high CPU time so it reduces the performance of the applications, but not enough to worry about it because exceptions help stabilize the programs.

Additional workloads because we need to create a new class for each exception.

Ideas for improving design:

To improve this design, you can use one of the error handling techniques in ASP.NET Core such as: Exception middleware or Exception filters.

To read about this site, you can visit these sites:

  1. Handle errors in ASP.NET Core web APIs.
  2. Handle errors in ASP.NET Core.
  3. Exception filters

Scenario 2 – Result Object Or Result Pattern

If we were to apply some design principles to our application such as Separation of Concerns or Single-Responsibility Principle (SRP) to enforce some of the more stringent rules to keep our program consistent.

We will find that the first scenario is not sufficient to apply these principles, so we must think in another direction to find a new way through which we can return more than one value to the Caller.

In this scenario, we will create a new object through which we can expand the dialogue between the Methods of Services and the Caller. The first thing that will come to our minds is:

What properties will this new class contain?

Simply, there are a lot of properties that we can add. But in our topic, we will add the most important properties and leave the rest of the properties to the needs of the scenario that you implement.

The first property added, of course, is the expected result of our service method, and the second property is information about the exception, and finally, we can add an additional property to alert the user about something that has happened or about something that needs to be done. On some service methods, we want to return the result with a warning. For example, in our example, we want to remind the user that he must change the password, as the result is just not sufficient for that, in such cases we use this property if we want to tell him how many failed attempts he made to log in … etc.

The final design for this class will look like this:

C#
/// <summary>
/// To identify the type of result in the Front-End app.
/// </summary>
public enum ResultKinds
{
    Success,
    Warning,
    Exception
}

public class ServiceResult<T> : DTO
{
    public ServiceResult(ResultKinds kind, T result, string warning, 
                         ExceptionDescriptions exceptionDescriptions)
    {
        Result = result;
        ExceptionDescriptions = exceptionDescriptions;
        Kind = kind;
        WarningDescription = warning;
    }

    public ServiceResult(T result) : this(ResultKinds.Success, result, null, null) { }
    public ServiceResult(T result, string warning) : 
           this(ResultKinds.Warning, result, warning, null) { }
    public ServiceResult(ExceptionDescriptions exceptionDescriptions) : 
           this(ResultKinds.Exception, default, null, exceptionDescriptions) { }

    /// <summary>
    /// In the Front-End program, we test the value of the Kind property
    /// For the object we received via Response,
    /// and on the basis of it we build the result for the user.
    /// </summary>
    public ResultKinds Kind { get; }

    /// <summary>
    /// The service result
    /// </summary>
    public T Result { get; }

    /// <summary>
    /// To show warnings in the in the Front-End app.
    /// </summary>
    public string WarningDescription { get; }

    /// <summary>
    /// The exception descriptions
    /// </summary>
    public ExceptionDescriptions ExceptionDescriptions { get; }

    public bool IsSuccess => Kind != ResultKinds.Exception;

    public static ServiceResult<T> Success(T result) => 
           new ServiceResult<T>(ResultKinds.Success, result, null, null);
    public static ServiceResult<T> Warning(T result, string warning) => 
           new ServiceResult<T>(ResultKinds.Warning, result, warning, null);
    public static ServiceResult<T> Exception(ExceptionDescriptions exceptionDescriptions) => 
         new ServiceResult<T>(ResultKinds.Exception, default, null, exceptionDescriptions);

    public static implicit operator T(ServiceResult<T> result) => result.Result;
    public static explicit operator ExceptionDescriptions(ServiceResult<T> result) => 
           result.ExceptionDescriptions;
    public static explicit operator Exception(ServiceResult<T> result) => 
           result.ExceptionDescriptions.Exception;
}

We have modified the type that the Authenticate Method will return. Instead of returning LoginDto, will return ServiceResult<LoginDto> of the type we previously talked about, and the final design for this LoginService will be as follows:

C#
public interface ILoginService : IService
{
    ServiceResult<LoginDto> Authenticate(string userName, string pw);
}

public class LoginService : Service, ILoginService
{
    public LoginService(IMapper mapper, IHostEnvironment environment) : 
           base(mapper) => Environment = environment;

    protected IHostEnvironment Environment { get; }

    public ServiceResult<LoginDto> Authenticate(string userName, string pw)
    {
        try
        {
            // This is just an example of the return type so we haven't
            // listed all the code this function needs to fetch data
            // from the database and the validation code.

            // ===== User with Warning =====
            if (userName == "user_a" && pw == "pw")
            {
                return ServiceResult<LoginDto>.Warning(
                    Map<Login, LoginDto>(new Login() { UserName = "default user" }),
                    "For more than ten months you have not changed your password, 
                     we recommend that you change it as soon as possible.");
            }

            // ===== Valid user =====
            if (userName == "user_b" && pw == "pw")
            {
                return ServiceResult<LoginDto>.Success(Map<Login, LoginDto>
                       (new Login() { UserName = "default user" }));
            }

            // ===== User with Exception =====
            if (userName == "user_c" && pw == "pw")
            {
                return ServiceResult<LoginDto>.Exception(new ExceptionDescriptions(
                            new ForbiddenException($"User {userName} is currently blocked."),
                            MethodBase.GetCurrentMethod().Name,
                            Environment));
            }
        }
        catch (Exception ex)
        {
            return ServiceResult<LoginDto>.Exception(new ExceptionDescriptions(
                    // Probably one of the exceptions we designed, 
                    // so we test the exception first.
                    ex as HttpStatusCodeException ?? 
                       new InternalServerErrorException(ex.Message, ex),
                    MethodBase.GetCurrentMethod().Name,
                    Environment));

            // 
            /*
             * ====================================================
             * During the troubleshooting process, we can throw other types, 
             * when analyzing this exception 'ex'.
             * ====================================================
             * if (ex is xxxException) {
             *      return ServiceResult<LoginDto>.Exception
                    (nnew ExceptionDescriptions
                    (ew xxx_Custme_Exception(ex.Message, ex), ...));
             * }
             * ...
             */
        }

        return ServiceResult<LoginDto>.Exception(new ExceptionDescriptions(
                    new BadRequestException
                    ("The username or password you entered is incorrect."),
                    MethodBase.GetCurrentMethod().Name,
                    Environment));
    }
}

Here, we will display the Controller, which we modified to suit the new way to call this method, and how will he perform the testing the Kind property of the result returned from the service method to show clear results to the user:

C#
public class LoginController : AppController<ILoginService>
{
    public LoginController(ILoginService service, IWebHostEnvironment environment) :
           base(service, environment) { }

    [AllowAnonymous]
    [HttpPost]
    public IActionResult Authenticate([FromBody] LoginModel model) =>
        Service
            .Authenticate(model.UserName, model.Password)
            .ToActionResult(this);
}

We will notice that the Controller is clean, clear and is only responsible for handling requests.

Here, we will be listing the Controller Base classes:

C#
public abstract class AppController<TService> : AppController where TService : IService
{
    protected AppController(TService service, IWebHostEnvironment environment) : 
                            base(environment) => Service = service;

    protected TService Service { get; }
}

[Route("api/[controller]")]
[ApiController]
public abstract class AppController : ControllerBase
{
    protected AppController(IWebHostEnvironment environment) => Environment = environment;

    public IWebHostEnvironment Environment { get; }

    /// <summary>
    /// Creates an <see cref="ObjectResult"/> object that produces an 
    /// <see cref="StatusCodes.Status403Forbidden"/> response.
    /// </summary>
    /// <returns>The created <see cref="ObjectResult"/> for the response.</returns>
    [NonAction]
    public virtual ObjectResult Forbidden([ActionResultObjectValue] object value) => 
           StatusCode(StatusCodes.Status403Forbidden, value);
    /// <summary>
    /// Creates an <see cref="ObjectResult"/> object that produces an 
    /// <see cref="StatusCodes.Status500InternalServerError"/> response.
    /// </summary>
    /// <returns>The created <see cref="ObjectResult"/> for the response.</returns>
    [NonAction]
    public virtual ObjectResult InternalServerError([ActionResultObjectValue] 
           object value) => StatusCode(StatusCodes.Status500InternalServerError, value);

    /// <summary>
    /// Creates an <see cref="ObjectResult"/> object that produces an 
    /// <see cref="StatusCodes.Status405MethodNotAllowed"/> response.
    /// </summary>
    /// <returns>The created <see cref="ObjectResult"/> for the response.</returns>
    [NonAction]
    public virtual ObjectResult MethodNotAllowed([ActionResultObjectValue] 
           object value) => StatusCode(StatusCodes.Status405MethodNotAllowed, value);
}

Finally, the extensions method for moving the result processing to another unit:

C#
public static class ServicesResultExtensions
{
    public static IActionResult ToActionResult<T>(this ServiceResult<T> result, 
           AppController controller, [CallerMemberName] string callerName = "") =>
        // C# 8.0
        // https://docs.microsoft.com/en-us/dotnet/csharp/
        // language-reference/operators/switch-expression
        // In the Front-End program, we test the value of the Kind property
        // For the object we received via Response,
        // and on the basis of it we build the result for the user.
        result?.Kind switch
        {
            ResultKinds.Exception =>
                    result.ExceptionDescriptions?.Exception switch
                    {
                        EntityNotFoundException _ => controller.NotFound
                        (new { result.Kind, KindName = result.Kind.ToString(), 
                        result.ExceptionDescriptions }),
                        InternalServerErrorException _ => controller.InternalServerError
                        (new { result.Kind, KindName = result.Kind.ToString(), 
                        result.ExceptionDescriptions }),
                        MethodNotAllowedException _ => controller.MethodNotAllowed
                        (new { result.Kind, KindName = result.Kind.ToString(), 
                        result.ExceptionDescriptions }),
                        UnprocessableEntityException _ => controller.UnprocessableEntity
                        (new { result.Kind, KindName = result.Kind.ToString(), 
                        result.ExceptionDescriptions }),
                        BadRequestException _ => controller.BadRequest(new 
                        { result.Kind, KindName = result.Kind.ToString(), 
                        result.ExceptionDescriptions }),
                        ForbiddenException _ => controller.Forbidden(new 
                        { result.Kind, KindName = result.Kind.ToString(), 
                        result.ExceptionDescriptions }),
                        _ => controller.InternalServerError(new { result.Kind, 
                        KindName = result.Kind.ToString(), result.ExceptionDescriptions }),
                    },
            ResultKinds.Warning => controller.Ok(new { result.Kind, 
            KindName = result.Kind.ToString(), result.Result, result.WarningDescription }),
            ResultKinds.Success => controller.Ok(new { result.Kind, 
            KindName = result.Kind.ToString(), result.Result }),
            _ => controller.InternalServerError(
                    new
                    {
                        Kind = ResultKinds.Exception,
                        KindName = result.Kind.ToString(),
                        ExceptionDescriptions = new ExceptionDescriptions
                        (new InternalServerErrorException(), callerName, 
                         controller.Environment)
                    })
        };
}

In this scenario, we were able to achieve the Separation of Concerns because we had ensured that our error logic went within our implementation logic, and we were also able to achieve the Single-Responsibility Principle (SRP) because we have noticed that the controllers are responsible for handling the requests and return an obvious response.

These are the results of the Postman:

Response.json

JavaScript
// Warning
{
    "kind": 1,
    "kindName": "Warning",
    "result": {
        "userName": "Warning user"
    },
    "warningDescription": "For more than ten months you have not changed your password, 
                           we recommend that you change it as soon as possible."
}
// Success
{
    "kind": 0,
    "kindName": "Success",
    "result": {
        "userName": "Valid user"
    }
}
// blocked User
{
    "kind": 2,
    "kindName": "Exception",
    "exceptionDescriptions": {
        "statusCode": 403,
        "status": "Forbidden",
        "title": "Authenticate",
        "detail": "User user_c is currently blocked.",
        "targetSite": "ServiceResult.AspectOriented.LoginService.Authenticate",
        "stackTrace": [
            "   at ServiceResult.AspectOriented.LoginService.Authenticate
                (String userName, String pw) in 
                C:\\My Projects\\ServiceResult\\src\\ServiceResult.AspectOriented\\
                LoginService.cs:line 20"
        ],
        "innerException": null
    }
}
// Bad Request
{
    "kind": 2,
    "kindName": "Exception",
    "exceptionDescriptions": {
        "statusCode": 400,
        "status": "BadRequest",
        "title": "Authenticate",
        "detail": "The username or password you entered is incorrect.",
        "targetSite": "ServiceResult.AspectOriented.LoginService.Authenticate",
        "stackTrace": [
            "   at ServiceResult.AspectOriented.LoginService.Authenticate
                (String userName, String pw) in C:\\My Projects\\ServiceResult\\
                src\\ServiceResult.AspectOriented\\LoginService.cs:line 66"
        ],
        "innerException": null
    }
}

Scenario 3 – All with Aspect-Oriented Programming Or Decorator Pattern

Aspect Oriented Programming (AOP): We will not talk about all the advantages of this design, but we will talk about the most prominent features. It is a very effective way to divide the work of the program into sections that are easy to lead, maintain and develop without harming other parts of the Modularity program. The main idea is to add new behavior to the existing code without making any changes in the code itself. The new code is supposed to be public so that it can be applied to any object and the object should know nothing about the behavior. AOP also allows the developer to apply Separation Of Cross-Cutting Concerns and facilitates a Single-Responsibility Principle (SRP).

Here in the program we will talk about the parts that we have modified, and as they talked at the beginning of this topic, you can download all the programs that we have developed for this article below.

First, we slightly modified to the service to return the expected value, and we made an asynchronous version of the method for testing asynchronous programming.

C#
public interface ILoginService : IService
{
    Task<LoginDto> AuthenticateAsync(string userName, string pw);
    LoginDto Authenticate(string userName, string pw);
}

public class LoginService : Service, ILoginService
{
    public LoginService(IMapper mapper) : base(mapper) { }

    [DebuggerHidden]
    public Task<LoginDto> AuthenticateAsync(string userName, string pw) => 
                          Task.Run(() => Authenticate(userName, pw));

    [DebuggerHidden]
    public LoginDto Authenticate(string userName, string pw)
    {
        try
        {
            // This is just an example of the return type so we haven't
            // listed all the code this function needs to fetch data
            // from the database and the validation code.

            // ===== User with Warning =====
            if (userName == "user_a" && pw == "pw")
            {
                // To follow the 'warning convention' between AOP and the service.
                throw new WarningException(
                       Map<Login, LoginDto>(new Login() { UserName = "Warning user" }),
                       "For more than ten months you have not changed your password, 
                        we recommend that you change it as soon as possible.");
            }

            // ===== Valid user =====
            if (userName == "user_b" && pw == "pw")
            {
                return Map<Login, LoginDto>(new Login() { UserName = "Valid user" });
            }

            // ===== User with Exception =====
            if (userName == "user_c" && pw == "pw")
            {
                throw new ForbiddenException($"User {userName} is currently blocked.");
            }
        }
        catch (Exception ex)
        {
            // Probably one of the exceptions we designed, so we test the exception first.
            throw ex as HttpStatusCodeException ?? new InternalServerErrorException
                  (ex.Message, ex);

            // 
            /*
             * ====================================================
             * During the troubleshooting process, we can throw other types, 
             * when analyzing this exception 'ex'.
             * ====================================================
             * if (ex is xxxException) {
             *      throw xxx_Custme_Exception(ex.Message, ex), ...));
             * }
             * ...
             */
        }

        throw new BadRequestException("The username or password you entered is incorrect.");
    }
}

We then created a new class inherited from DispatchProxy. This type has been present in .NET Core from the start of the platform and provides a mechanism for creating Proxy Objects and processing their Dispatch Method. And a Proxy Object can be created from it by using this code:

C#
var proxy = DispatchProxy.Create<Interface, Proxy>();

Now the important part of this program is implementing the GenericDecorator to be used in all programs. The code will come later, which is a subclass from DispatchProxy and then we create a new subclass to match the new behavior we want to add to the services. In AOP, it's called the Aspect.

An aspect is the part of the app that crosscuts the basic concerns of multiple beings, therefore violating its separation of concerns that tries to encapsulate unrelated functions.

The Generic Decorator or Generic Proxy:

C#
public class GenericDecorator<T> : DispatchProxy
{
    protected T Decorated { get; private set; }

    /// <summary>
    /// Whenever any method on the generated proxy type is called,
    /// this method is invoked before the target method to dispatch control.
    /// </summary>
    /// <remarks>Use this interceptor method to initialize your plan 
    /// before inject any behavior into the proxy.</remarks>
    /// <param name="targetMethod">The method the caller invoked.</param>
    /// <param name="args">The arguments the caller passed to the method.</param>
    /// <param name="methodKind">The kind of method.</param>
    protected virtual void BeforeInvoke(MethodInfo targetMethod, object[] args, 
              MethodKind methodKind) { }

    /// <summary>
    /// Whenever any method on the generated proxy type is called,
    /// this method is invoked after the target method to dispatch control.
    /// </summary>
    /// <remarks>Use this interceptor method to inject behavior into the proxy.</remarks>
    /// <param name="targetMethod">The method the caller invoked.</param>
    /// <param name="args">The arguments the caller passed to the method.</param>
    /// <param name="methodKind">The kind of method.</param>
    /// <param name="result">The object returned from the target method.</param>
    /// <returns>The object to return to the caller, or null for void methods.</returns>
    protected virtual object AfterInvoke(MethodInfo targetMethod, object[] args, 
              MethodKind methodKind, object result) => result;

    /// <summary>Executed when an exception occurred.</summary>
    /// <param name="exception">The exception that occurred.</param>
    /// <param name="methodInfo">The function that executed and issued this exception.
    /// </param>
    /// <param name="handled">TReturn true when handled the exception, 
    /// otherwise false</param>
    /// <returns>The object to return to the caller, or null for void methods.</returns>
    protected virtual object OnException(Exception exception, MethodInfo methodInfo, 
              out bool handled)
    {
        handled = false;
        return null;
    }

    protected override object Invoke(MethodInfo targetMethod, object[] args)
    {
        var getAwaiterMethod = targetMethod.ReturnType.GetMethod(nameof(Task.GetAwaiter));

        try
        {
            object result = null;
            var methodKind = targetMethod.GetKind();

            BeforeInvoke(targetMethod, args, methodKind);

            // Check if method is async
            if (getAwaiterMethod != null)
            {
                if (targetMethod.ReturnType.IsGenericType)
                {
                    dynamic awaitable = targetMethod.Invoke(Decorated, args);

                    result = awaitable.GetAwaiter().GetResult();
                    //Task.Run(() => { }).GetAwaiter();
                    result = AfterInvoke(targetMethod, args, methodKind, result);
                    result = CreateTask(targetMethod, result);
                }
                else
                {
                    dynamic awaitable = targetMethod.Invoke(Decorated, args);

                    awaitable.GetAwaiter().GetResult();
                    result = Task.CompletedTask;
                }
            }
            else
            {
                if (targetMethod.ReturnType == typeof(void))
                {
                    targetMethod.Invoke(Decorated, args);
                }
                else
                {
                    result = targetMethod.Invoke(Decorated, args);
                    result = AfterInvoke(targetMethod, args, methodKind, result);
                }
            }

            return result;
        }
        catch (Exception ex)
        {
            // Check if ex is TargetInvocationException or AggregateException.
            ex = ex.InnerException ?? ex;

            var result = OnException(ex, targetMethod, out var handled);

            result = getAwaiterMethod is null ? result : CreateTask(targetMethod, result);
            return handled ? result : throw ex;
        }
    }

    protected object CreateTask(MethodInfo targetMethod, object result) => 
              CreateTask(GetMethodReturnType(targetMethod), result);

    /// <summary>
    /// Return the type from the method, void if the method was Task, 
    /// T if the method was Task<T>.
    /// </summary>
    protected Type GetMethodReturnType(MethodInfo targetMethod)
    {
        //if (targetMethod.ReturnType == typeof(void)) { return null; }

        if (typeof(Task).IsAssignableFrom(targetMethod.ReturnType))
        {
            return targetMethod.ReturnType.IsGenericType
                ? targetMethod.ReturnType.GetGenericArguments()[0]
                : typeof(void);
        }

        return targetMethod.ReturnType;
    }
    protected object CreateTask(Type genericType, object result)
    {
        var fromResult = typeof(Task).GetMethod(nameof(Task.FromResult), 
                         BindingFlags.Public | BindingFlags.Static);

        return fromResult.MakeGenericMethod(genericType).Invoke(null, new object[] 
               { result });
        //return Task.FromResult((dynamic)result);
    }

    protected virtual void SetParameters(T original) => 
         Decorated = original ?? throw new ArgumentNullException(nameof(original));
}

But there is a problem in this scenario in that it is not safe in concurrency applications (Multithreading) because we are saving the ‘Object states’ in the global variables of this object so we will not implement it because of the risks that we will face.

- The other scenario, which we will implement: Creating a new type in runtime that inherits the original object to the expected result of service methods and inserts this information into it.

The disadvantages of this scenario is that it is somewhat difficult and requires some experience in System.Reflection.Emit, or we can use the Code Generation with Roslyn. In our topic, we used this System.Reflection.Emit.

We will put the code here, but we will not explain it because it is a big topic and outside the scope of our topic, but if you want to know more about System.Reflection.Emit, check this link.

We will explain how we can use the Object Builder that we have created, so that we can develop it in the future and transfer it to other projects if we want to.

Another disadvantage of this scenario is that we will not be able to get a Null result because we are always returning the new object that we created in the runtime. However, we added a new property so that we can see if the original result is Null or not.

As for the advantage that we will reap:

  • There are no problems with Concurrency (Multithreading).
  • The Separation Of Concerns, by transferring the responsibility of creating the object in the runtime to another object.
  • Single-Responsibility Principle (SRP): The ResultPatternAspect is only responsible for processing the result and creating a new result that fits into this scenario that we are implementing.
  • By applying the previous two principles, we have divided our program into small parts, or in other words, we are close to achieving modularity in our program.

The Result Pattern Aspect

By applying the principle of Programming for Interface not implementation, you will notice that we rely a lot on Interface to make our program based on Abstraction and not on Concrete Objects. Program behavior can also be changed in Runtime, and it also helps us write programs that are much better from a maintenance point of view. To make the Object deal with the methods it only needs, this is one of the principles of SOLID, which is the Interface Segregation (ISP).

C#
/// <summary>
/// To distinguish the services that we have created via the ResultPatternAspect
/// </summary>
public interface IResultPatternService { }

public interface IResultPatternAspect<TService> : IResultPatternService
{
    IResultPatternAspect<TService>
    Initialize(TService service, IObjectBuilder objectBuilder,
               IHostEnvironment environment);
}

public class ResultPatternAspect<TService> :
GenericDecorator<TService>, IResultPatternService, IResultPatternAspect<TService>
{
    private IObjectBuilder _objectBuilder;
    private IHostEnvironment _hostEnvironment;

    // protected override void BeforeInvoke(MethodInfo targetMethod,
    object[] args, MethodKind methodKind) =>
                   base.BeforeInvoke(targetMethod, args, methodKind);
    protected override object AfterInvoke(MethodInfo targetMethod,
    object[] args, MethodKind methodKind, object result)
    {
        if (methodKind != MethodKind.Method)
        {
            return result;
        }

        var dynamicResult = CreateProxyToObject(targetMethod, result);

        dynamicResult.Kind = ResultKinds.Success;
        dynamicResult.WarningDescription = null;
        dynamicResult.ExceptionDescriptions = null;

        return dynamicResult;
    }

    protected override object OnException(Exception exception,
                       MethodInfo methodInfo, out bool handled)
    {
        var warningException = exception as WarningException;

        handled = true;

        var dynamicResult = CreateProxyToObject(methodInfo, warningException?.Result);

        dynamicResult.Kind = warningException is null ?
                             ResultKinds.Exception : ResultKinds.Warning;
        dynamicResult.WarningDescription = warningException?.Message;
        dynamicResult.ExceptionDescriptions = new ExceptionDescriptions
        (exception as ServiceException ?? new InternalServerErrorException
        (exception.Message, exception), methodInfo.Name, _hostEnvironment);

        return dynamicResult;
    }

    private IResultPatternProxy
            CreateProxyToObject(MethodInfo targetMethod, object result)
    {
        var returnType = GetMethodReturnType(targetMethod);
        var dynamicResult = _objectBuilder.CreateObject(CreateTypeName(returnType),
        null, returnType, new[] { typeof(IResultPatternProxy) }, true)
                          as IResultPatternProxy;

        if (result != null)
        {
            var mapperConfiguration = new AutoMapper.MapperConfiguration
            (config => config.CreateMap(returnType, dynamicResult.GetType()));
            var mapper = new AutoMapper.Mapper(mapperConfiguration);

            mapper.Map(result, dynamicResult);
            dynamicResult.IsNull = false;

            var hh = returnType.IsAssignableFrom(dynamicResult.GetType());
        }

        dynamicResult.IsNull = true;

        return dynamicResult;
    }

    private static string CreateTypeName(Type type) =>
    $"_PROXY_RESULT_PATTERN_{type.Name.ToUpper()}_";

    IResultPatternAspect<TService> IResultPatternAspect<TService>.Initialize
    (TService service, IObjectBuilder objectBuilder, IHostEnvironment environment)
    {
        SetParameters(service);
        _objectBuilder = objectBuilder;
        _hostEnvironment = environment;

        return this;
    }
}

In order to facilitate the process of creating an object from the ResultPatternAspect, we applied the Factory Pattern to facilitate the process of modifying the strategy of creating this aspect.

C#
public interface IResultPatternAspecFactory
{
    TService Create<TService>(TService service);
}
public class ResultPatternAspecFactory : IResultPatternAspecFactory
{
    public ResultPatternAspecFactory(IServiceProvider serviceProvider) => 
                                     ServiceProvider = serviceProvider;

    public IServiceProvider ServiceProvider { get; }

    public TService Create<TService>(TService service)
    {
        var objectBuilder = ServiceProvider.GetService<IObjectBuilder>();
        var environment = ServiceProvider.GetService<IHostEnvironment>();
        var proxy = DispatchProxy.Create<TService, 
        ResultPatternAspect<TService>>() as IResultPatternAspect<TService>;

        proxy.Initialize(service, objectBuilder, environment);

        return proxy is TService serviceProxy ? serviceProxy : service;
    }
}

We'll talk about how to register objects in ASP.NET Core 3.1 Dependency Injection later.

Dynamic Object Builder

The Builder Pattern allows us to create complex objects step by step. It also allows us to produce different types and representations of an object using the same building code.

Now let's talk a little bit about how to use SimpleDynamicObjectBuilder, first we will show its code:

C#
public class BuilderPropertyInfo
{
    public string Name { get; set; }
    public Type Type { get; set; }
    public bool IsInterfaceImplementation { get; set; }
}

public interface IObjectBuilder
{
    object CreateObject(string name, BuilderPropertyInfo[] properties = null,
    Type baseClass = null, Type[] interfaces = null,
                           bool autoGenerateInterfaceproperties = false);
    TInterface CreateObject<TBase, TInterface>(string name) where TBase : class, new();
    TInterface CreateObject<TBase, TInterface>(string name,
    BuilderPropertyInfo[] properties = null) where TBase : class, new();
    TInterface CreateObject<TInterface>(string name);

    TBase CreateObject<TBase>(string name, BuilderPropertyInfo[] properties = null)
    where TBase : class, new();
    TBase CreateObject<TBase>(string name, BuilderPropertyInfo[] properties = null,
    Type[] interfaces = null, bool autoGenerateInterfaceproperties = false)
                              where TBase : class, new();
}

/// <summary>
/// This is a special builder for our scenario,
/// but we can do more on System.Reflection.Emit.
/// For more details see
/// <a href="https://livebook.manning.com/book/
/// metaprogramming-in-dot-net/about-this-book/">Metaprogramming
/// in .NET Book</a>.
/// </summary>
public class SimpleDynamicObjectBuilder : IObjectBuilder
{
    private readonly AssemblyBuilder _assemblyBuilder;
    private readonly ModuleBuilder _moduleBuilder;

    public SimpleDynamicObjectBuilder(string assemblyName)
    {
        _assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly
        (new AssemblyName(assemblyName), AssemblyBuilderAccess.Run);
        _moduleBuilder = _assemblyBuilder.DefineDynamicModule("MainModule");
    }

    public SimpleDynamicObjectBuilder() : this(Guid.NewGuid().ToString()) { }

    public TInterface CreateObject<TInterface>(string name)
    {
        var interfaceType = typeof(TInterface);

        if (!interfaceType.IsInterface) { return default; }

        return CreateObject(name, null, null, new Type[]
        { interfaceType }, true) is TInterface @interface ? @interface : default;
    }

    public TInterface CreateObject<TBase, TInterface>(string name)
    where TBase : class, new() => CreateObject<TBase, TInterface>(name, null);

    public TInterface CreateObject<TBase, TInterface>(string name,
    BuilderPropertyInfo[] properties = null) where TBase : class, new()
    {
        var interfaceType = typeof(TInterface);

        if (!interfaceType.IsInterface) { return default; }

        return CreateObject(name, properties, typeof(TBase),
        new Type[] { interfaceType }, true) is TInterface @interface ?
                     @interface : default;
    }

    public TBase CreateObject<TBase>(string name,
    BuilderPropertyInfo[] properties = null) where TBase : class,
    new() => CreateObject<TBase>(name, properties);
    public TBase CreateObject<TBase>(string name,
    BuilderPropertyInfo[] properties = null, Type[] interfaces = null,
    bool autoGenerateInterfaceproperties = false) where TBase : class, new() =>
        CreateObject(name, properties, typeof(TBase), interfaces,
        autoGenerateInterfaceproperties) as TBase;

    public object CreateObject(string name, BuilderPropertyInfo[] properties = null,
           Type baseClass = null, Type[] interfaces = null,
           bool autoGenerateInterfaceproperties = false)
    {
        // To avoid creating the class again.
        var definedType = Array.Find(_moduleBuilder.GetTypes(), x => x.Name == name);

        if (definedType != null)
        {
            return Activator.CreateInstance(definedType);
        }

        var dynamicClass = DefineType(name, baseClass, interfaces);

        CreateDefaultConstructor(dynamicClass);

        if (properties?.Length > 0)
        {
            foreach (var property in properties)
            {
                CreateProperty(dynamicClass, property);
            }
        }

        if (interfaces?.Length > 0 && autoGenerateInterfaceproperties)
        {
            foreach (var property in interfaces
                                        .SelectMany(x => x.GetProperties())
                                        .Select(x => new BuilderPropertyInfo()
                                        {
                                            Name = x.Name,
                                            Type = x.PropertyType,
                                            IsInterfaceImplementation = true
                                        })
                                        .ToArray())
            {
                CreateProperty(dynamicClass, property);
            }
        }

        return Activator.CreateInstance(dynamicClass.CreateType());
    }

    private TypeBuilder DefineType(string name, Type baseClass = null,
    Type[] interfaces = null) => _moduleBuilder.DefineType(name,
            TypeAttributes.Public |
            TypeAttributes.Class |
            TypeAttributes.AutoClass |
            TypeAttributes.AnsiClass |
            TypeAttributes.BeforeFieldInit |
            TypeAttributes.AutoLayout,
            baseClass == typeof(void) ? null : baseClass,
            interfaces);

    private ConstructorBuilder CreateDefaultConstructor(TypeBuilder typeBuilder) =>
        typeBuilder.DefineDefaultConstructor(MethodAttributes.Public |
        MethodAttributes.SpecialName | MethodAttributes.RTSpecialName);

    private PropertyBuilder CreateProperty(TypeBuilder typeBuilder,
            BuilderPropertyInfo propertyInfo)
    {
        var fieldBuilder = typeBuilder.DefineField("_" +
        propertyInfo.Name, propertyInfo.Type, FieldAttributes.Private);
        var propertyBuilder = typeBuilder.DefineProperty(propertyInfo.Name,
        PropertyAttributes.HasDefault, propertyInfo.Type, null);
        var methodAttributes =
            MethodAttributes.Public |
            MethodAttributes.SpecialName |
            MethodAttributes.HideBySig |
            (propertyInfo.IsInterfaceImplementation ? MethodAttributes.Virtual : 0);

        // get => _privateField;
        var getPropMthdBldr = typeBuilder.DefineMethod("get_" +
        propertyInfo.Name, methodAttributes, propertyInfo.Type, Type.EmptyTypes);
        var getIl = getPropMthdBldr.GetILGenerator();

        getIl.Emit(OpCodes.Ldarg_0);
        getIl.Emit(OpCodes.Ldfld, fieldBuilder);
        getIl.Emit(OpCodes.Ret);

        // set => _privateField = value;
        var setPropMthdBldr = typeBuilder.DefineMethod("set_" +
        propertyInfo.Name, methodAttributes, null, new[] { propertyInfo.Type });
        var setIl = setPropMthdBldr.GetILGenerator();

        setIl.Emit(OpCodes.Ldarg_0);
        setIl.Emit(OpCodes.Ldarg_1);
        setIl.Emit(OpCodes.Stfld, fieldBuilder);
        setIl.Emit(OpCodes.Ret);

        // Set get and set method to property
        propertyBuilder.SetGetMethod(getPropMthdBldr);
        propertyBuilder.SetSetMethod(setPropMthdBldr);

        return propertyBuilder;
    }
}

To create a Dynamic Instance in the Runtime, we just need to create an Instance from this DynamicObjectBuilder and then follow these simple steps:

C#
var dynamicObjectBuilder = new DynamicObjectBuilder("Domain_Assembly");

Here, we have created some functions to create a dynamic object in the runtime:

C#
public class User
{
    public string Name { get; set; }
}

public interface IInterface
{
    public string Property1 { get; set; }
}

private static string CreateTypeName(Type type) => 
               $"_PROXY_RESULT_PATTERN_{type.Name.ToUpper()}_";

private static void ObjectWithInterface(DynamicObjectBuilder dynamicObjectBuilder)
{
    var dynamicProxy = dynamicObjectBuilder.CreateObject<IInterface>
                       (CreateTypeName(typeof(IInterface)));

    // Test the object.
    if (dynamicProxy is IInterface resultPattern)
    {
        resultPattern.Property1 = "Object With Interface";
    }

    foreach (var property in dynamicProxy.GetType().GetProperties())
    {
        Console.WriteLine($"{property.Name}: {property.GetValue(dynamicProxy)}");
    }
}

private static void ObjectWithBaseClassAndInterfaceAndCustomProperty
               (DynamicObjectBuilder dynamicObjectBuilder)
{
    var dynamicProxy = dynamicObjectBuilder.CreateObject<User, IInterface>
        (
            CreateTypeName(typeof(User)),
            new[]
            {
                new BuilderPropertyInfo {Name = "Test_Custom_Property", 
                Type = typeof(int), IsInterfaceImplementation = false},
            }
        );

    // Test the object.
    if (dynamicProxy is IInterface resultPattern)
    {
        resultPattern.Property1 = "Object With Base Class And Interface And Custom Property";
    }

    if (dynamicProxy is User login)
    {
        login.Name = "dynamic proxy user";
    }

    foreach (var property in dynamicProxy.GetType().GetProperties())
    {
        Console.WriteLine($"{property.Name}: {property.GetValue(dynamicProxy)}");
    }
}

To use the ObjectWithBaseClassAndInterfaceAndCustomProperty and ObjectWithInterface that we created earlier, we used long names to illustrate:

C#
private static void Main()
{
    var dynamicObjectBuilder = new DynamicObjectBuilder("Domain_Assembly");

    Console.WriteLine("***** Object With Base Class & Interface *****");
    ObjectWithBaseClassAndInterfaceAndCustomProperty(dynamicObjectBuilder);
    Console.ReadLine();

    ObjectWithInterface(dynamicObjectBuilder);
    Console.ReadLine();

}

How to register objects in ASP.NET Core 3.1 Dependency Injection.

We created new extension to register all objects necessary to fulfill this scenario.

C#
public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddResultPattern(this IServiceCollection services)
    {
        services.AddSingleton<IObjectBuilder, SimpleDynamicObjectBuilder>();
        //services.AddSingleton(typeof(IResultPatternAspect<>), typeof(ResultPatternAspect<>));
        services.AddSingleton<IResultPatternAspecFactory, ResultPatternAspecFactory>();

        return services;
    }
}

Factory Injection In ASP.NET CORE

In our main program and in the Startup file, we used this extension and registered the Services using Factory Injection In ASP.NET Core 3.1 to be able to use the ResultPatternAspec to create a Service Proxy.

When we talk about Factory, we're referring to a mechanism within the program that is responsible for creating instantiating classes and returning those instances.

C#
public class Startup
{
    public Startup(IConfiguration configuration) => Configuration = configuration;

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. 
    // Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAutoMapper
                (
                    configAction =>
                    {
                        configAction
                            .CreateMap<Login, LoginDto>()
                            .ReverseMap()
                            .ForMember((dest) => dest.Id, _ => Guid.NewGuid());
                        // To Avoid Recursive Mapping
                        // configAction.ForAllMaps((map, exp) => exp.MaxDepth(2));
                    },
                    Assembly.GetExecutingAssembly()
                );

        services
            // Call this to  inject all the needs of the 
            // ServiceResult.AspectOriented Scenario.
            .AddResultPattern()
            // Inject proxy for service
            .AddTransient((serviceProvider) =>
            {
                // Use Factory Injection In ASP.NET 3.1.
                var resultPatternAspecFactory = 
                    serviceProvider.GetService<IResultPatternAspecFactory>();
                var mapper = serviceProvider.GetService<IMapper>();

                // Create Service Proxy
                return resultPatternAspecFactory.Create<ILoginService>
                                                 (new LoginService(mapper));
            });

        services.AddControllers();
    }

    // This method gets called by the runtime. Use this method to configure the 
    // HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();

        app.UseAuthorization();

        app.UseEndpoints(endpoints => endpoints.MapControllers());
    }
}

Just as we created ServiceResultExtensions, it contains a GetResult<T> function whose mission is to read the properties and create result instances from the object that we created in the runtime using SimpleDynamicObjectBuilder.

C#
public static class ServiceResultExtensions
{
    // Probably one of the exceptions we designed, so we test the exception first.
    public static ServiceResult<T> GetResult<T>(this T result) => 
                                   result is IResultPatternProxy proxy
            ? CreateServiceResult<T>(proxy.Kind, result, 
              proxy.WarningDescription, proxy.ExceptionDescriptions)
            : ServiceResult<T>.Success(result);

    private static ServiceResult<T> CreateServiceResult<T>
            (ResultKinds kind, object result, string warning, 
            ExceptionDescriptions exceptionDescriptions) =>
        Activator.CreateInstance(typeof(ServiceResult<>).MakeGenericType(typeof(T)), 
            kind, result, warning, exceptionDescriptions) as ServiceResult<T>;
}

We will notice that the Controllers are still clean and clear, and their task is limited to handling requests only.

C#
public class LoginController : AppController<ILoginService>
{
    public LoginController(ILoginService service, IWebHostEnvironment environment) : 
           base(service, environment) { }

    [AllowAnonymous]
    [HttpPost("AuthenticateAsync")]
    public async Task<IActionResult> AuthenticateAsync([FromBody] LoginModel model) =>
        (
            await Service
                    .AuthenticateAsync(model.UserName, model.Password)
                    .ConfigureAwait(false)
        )
        .GetResult()
        .ToActionResult(this);

    [AllowAnonymous]
    [HttpPost("Authenticate")]
    public IActionResult Authenticate([FromBody] LoginModel model) =>
        Service
            .Authenticate(model.UserName, model.Password)
            .GetResult()
            .ToActionResult(this);
}

Conclusion

In this post, I showcased various ways to return a meaningful result for Caller.

This code works well in the scenarios we have explained. If you have any examples of situations where this code does not work, or have ideas on how to improve this code, then I hope you explain this in any way.

Hope you liked the article. Please share your opinions in the comments section below.

You can find the source code on GitHub.

History

  • 12th October, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Syrian Arab Republic Syrian Arab Republic
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --