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.
| What type of result should the service method return to the caller?
|
- What type will we return when calling Methods of Services?
- What properties will it contain?
- 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:
public class xxxService : Service, IxxxService
{
public IModel GetByID(Guid id)
{
return new xxxModel() { … };
} public int GetLastIndex()
{
return 0;
} 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.
public class xxxService : Service, IxxxService
{
public IModel GetByID(Guid id)
{
try{
return new xxxModel() { … };
}catch (Exception){
return null;
}
} public int GetLastIndex()
{
try{
return 1;
}catch (Exception){
return -1;
}
}
public bool? Exists(IModel model)
{
try{
return true;
}catch (Exception){
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
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();
}
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
public class LoginService : Service, ILoginService
{
public LoginService(IMapper mapper) : base(mapper) { }
public LoginDto Authenticate(string userName, string pw)
{
try
{
return userName == "user" && pw == "pw" ? Map<Login, LoginDto>(new Login()
{ UserName = "default user" }) : null;
}
catch
{
return null;
}
}
}
The Controller Class
[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.
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:
try
{
}
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
public class ServiceException : Exception
{
public ServiceException() { }
public ServiceException(string message) : base(message) { }
public ServiceException(string message, Exception innerException) :
base(message, innerException) { }
}
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
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) { }
}
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
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
{
if (userName == "user_a" && pw == "pw")
{
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.");
}
if (userName == "user_b" && pw == "pw")
{
return Map<Login, LoginDto>(new Login() { UserName = "Valid user" });
}
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)
{
throw ex as HttpStatusCodeException ??
new InternalServerErrorException(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:
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);
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:
- Handle errors in ASP.NET Core web APIs.
- Handle errors in ASP.NET Core.
- Exception filters
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:
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) { }
public ResultKinds Kind { get; }
public T Result { get; }
public string WarningDescription { get; }
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:
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
{
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.");
}
if (userName == "user_b" && pw == "pw")
{
return ServiceResult<LoginDto>.Success(Map<Login, LoginDto>
(new Login() { UserName = "default user" }));
}
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(
ex as HttpStatusCodeException ??
new InternalServerErrorException(ex.Message, ex),
MethodBase.GetCurrentMethod().Name,
Environment));
}
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:
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:
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; }
[NonAction]
public virtual ObjectResult Forbidden([ActionResultObjectValue] object value) =>
StatusCode(StatusCodes.Status403Forbidden, value);
[NonAction]
public virtual ObjectResult InternalServerError([ActionResultObjectValue]
object value) => StatusCode(StatusCodes.Status500InternalServerError, value);
[NonAction]
public virtual ObjectResult MethodNotAllowed([ActionResultObjectValue]
object value) => StatusCode(StatusCodes.Status405MethodNotAllowed, value);
}
Finally, the extensions method for moving the result processing to another unit:
public static class ServicesResultExtensions
{
public static IActionResult ToActionResult<T>(this ServiceResult<T> result,
AppController controller, [CallerMemberName] string callerName = "") =>
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
{
"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."
}
{
"kind": 0,
"kindName": "Success",
"result": {
"userName": "Valid 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
}
}
{
"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
}
}
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.
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
{
if (userName == "user_a" && pw == "pw")
{
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.");
}
if (userName == "user_b" && pw == "pw")
{
return Map<Login, LoginDto>(new Login() { UserName = "Valid user" });
}
if (userName == "user_c" && pw == "pw")
{
throw new ForbiddenException($"User {userName} is currently blocked.");
}
}
catch (Exception ex)
{
throw ex as HttpStatusCodeException ?? new InternalServerErrorException
(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:
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:
public class GenericDecorator<T> : DispatchProxy
{
protected T Decorated { get; private set; }
protected virtual void BeforeInvoke(MethodInfo targetMethod, object[] args,
MethodKind methodKind) { }
protected virtual object AfterInvoke(MethodInfo targetMethod, object[] args,
MethodKind methodKind, object result) => result;
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);
if (getAwaiterMethod != null)
{
if (targetMethod.ReturnType.IsGenericType)
{
dynamic awaitable = targetMethod.Invoke(Decorated, args);
result = awaitable.GetAwaiter().GetResult();
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)
{
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);
protected Type GetMethodReturnType(MethodInfo targetMethod)
{
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 });
}
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).
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;
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.
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:
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();
}
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)
{
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);
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);
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);
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:
var dynamicObjectBuilder = new DynamicObjectBuilder("Domain_Assembly");
Here, we have created some functions to create a dynamic object in the runtime:
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)));
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},
}
);
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:
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.
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddResultPattern(this IServiceCollection services)
{
services.AddSingleton<IObjectBuilder, SimpleDynamicObjectBuilder>();
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.
public class Startup
{
public Startup(IConfiguration configuration) => Configuration = configuration;
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddAutoMapper
(
configAction =>
{
configAction
.CreateMap<Login, LoginDto>()
.ReverseMap()
.ForMember((dest) => dest.Id, _ => Guid.NewGuid());
},
Assembly.GetExecutingAssembly()
);
services
.AddResultPattern()
.AddTransient((serviceProvider) =>
{
var resultPatternAspecFactory =
serviceProvider.GetService<IResultPatternAspecFactory>();
var mapper = serviceProvider.GetService<IMapper>();
return resultPatternAspecFactory.Create<ILoginService>
(new LoginService(mapper));
});
services.AddControllers();
}
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
.
public static class ServiceResultExtensions
{
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.
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);
}
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