(CORE) CORE (2022)

Simplest way to create tracing and authorization layers in Backend API. Special attribute to arrange API for authorization layers.

1. Trace layer.


Each important Backend API (working with money for example) or for debugging purposes need tracing input parameters and output result. In this page I want to describe my way.

Firstly, need to understand you can not use Request to tracing, because any API usually perform API from another controllers, in this case HttpContext.Request will be nothing. Second point that Net Core use PipeReader and special reader BodyReader. Nobody understanding how to use it with Kestrel, even developers of this reader. This two restriction getting us simplest way to receive finally result quickly, safe and without trouble.

So, in controller's methods I use this pattern.



In the beginning of each method I perform the same line:


TraceRequest(Reflection.MethodBase.GetCurrentMethod.Name, Model)

And in the normal end of each methods I use line:


TraceResult(_WithResult, _RequestTraceID, RetResult)

Goal of this function is tracing intercontrollers calls (when Request is nothing). This is simple functions based on Reflection.


   1:  Imports System.Reflection
   2:  Imports System.Runtime.CompilerServices
   3:  Imports BackendAPI.Model
   4:  Imports BackendAPI.Services
   5:  Imports Microsoft.AspNetCore.Mvc
   6:  Imports Microsoft.EntityFrameworkCore
   7:  Imports BackendAPI.Helper
   8:  Imports Renci.SshNet.Sftp
   9:   
  10:  Namespace WebApi.Controllers
  11:      Partial Friend Module Tracer
  12:          Private Property _DB As ApplicationDbContext
  13:          Private Property _Trace As Boolean = False
  14:          Private Property _WithResult As Boolean = False
  15:          Private Property _UserService As IUserService
  16:          Private Property _RequestTraceID As FieldInfo
  17:          ''' <summary>
  18:          ''' Require definition 5 property in controller - _DB As ApplicationDbContext,_Trace As Boolean,_WithResult As Boolean,_UserService,_RequestTraceID
  19:          ''' Can be call in start of each API
  20:          ''' </summary>
  21:          <Extension>
  22:          Public Sub TraceRequest(Controller As ControllerBase, MethodName As String, Model As Object)
  23:              Dim Y As Type = Controller.GetType
  24:              For Each OneProp As FieldInfo In Y.GetRuntimeFields
  25:                  If OneProp.Name = "_DbForTrace" Then
  26:                      _DB = OneProp.GetValue(Controller)
  27:                  ElseIf OneProp.Name = "_Trace" Then
  28:                      _Trace = OneProp.GetValue(Controller)
  29:                  ElseIf OneProp.Name = "_WithResult" Then
  30:                      _WithResult = OneProp.GetValue(Controller)
  31:                  ElseIf OneProp.Name = "_UserService" Then
  32:                      _UserService = OneProp.GetValue(Controller)
  33:                  ElseIf OneProp.Name = "_RequestTraceID" Then
  34:                      _RequestTraceID = OneProp
  35:                  End If
  36:              Next
  37:              If _DB IsNot Nothing And _UserService IsNot Nothing Then
  38:                  If _Trace Then
  39:                      Dim TraceID As String = ""
  40:                      If Controller.HttpContext IsNot Nothing Then
  41:                          TraceID = Controller.Request.HttpContext.TraceIdentifier
  42:                      Else
  43:                          TraceID = Guid.NewGuid.ToString
  44:                      End If
  45:                      _RequestTraceID.SetValue(Controller, TraceID) 
  46:                      Dim UserName As String = ""
  47:                      If Controller.HttpContext IsNot Nothing Then
  48:                          If Controller.HttpContext.Request.Headers("JwtUserName").Count > 0 Then
  49:                              Dim CurUsers As ApplicationUser = _UserService.GetCurrentUser(Controller.HttpContext.Request.Headers("JwtUserName")(0))
  50:                              UserName = CurUsers.UserName
  51:                          End If
  52:                      End If
  53:                      Dim ApiName As String = ""
  54:                      If Controller.Request IsNot Nothing Then
  55:                          ApiName = Controller.Request.HttpContext.Request.Path.Value
  56:                      Else
  57:                          ApiName = $"/{Y.Name}/{MethodName}"
  58:                      End If
  59:                      Dim RequestPrm As String = Newtonsoft.Json.JsonConvert.SerializeObject(Model)
  60:                      _DB = _DB.ReOpen
  61:                      Sql.ExecNonQuery(_DB, $"INSERT INTO `BackendApiTraceLog`(`TraceID`,`CrDate`,`UserName`,`ApiName`,`Request`) VALUES ('{TraceID}',Now(),{If(String.IsNullOrEmpty(UserName), "null", $"'{UserName}'")},'{ApiName}','{RequestPrm.Replace("'", "^")}');")
  62:                      _DB.Database.GetDbConnection.Close()
  63:                  End If
  64:              End If
  65:          End Sub
  66:      End Module
  67:  End Namespace
  68:   

   1:  Imports Microsoft.EntityFrameworkCore
   2:  Imports BackendAPI.Helper
   3:  Imports Microsoft.AspNetCore.Http
   4:  Imports Renci.SshNet.Sftp
   5:  Imports System.Runtime.CompilerServices
   6:  Imports BackendAPI.Model
   7:   
   8:  Namespace WebApi.Controllers
   9:      Partial Friend Module Tracer
  10:   
  11:          ''' <summary>
  12:          ''' Additional result tracer for directly call API from another API   
  13:          ''' </summary>
  14:          <Extension>
  15:          Sub TraceResult(_DB As ApplicationDbContext, SaveToDb As Boolean, TraceID As String, Result As Object)
  16:              If SaveToDb Then
  17:                  Dim Guid As Guid
  18:                  Dim ResultStr As String = Newtonsoft.Json.JsonConvert.SerializeObject(Result)
  19:                  _DB = _DB.ReOpen
  20:                  Sql.ExecNonQuery(_DB, $"Update `cryptochestmax`.`BackendApiTraceLog` SET Response='{ResultStr.Replace("'", "^")}'  where `TraceID`='{TraceID}';")
  21:                  _DB.Database.GetDbConnection.Close()
  22:                  _DB.Database.GetDbConnection.Dispose()
  23:              End If
  24:          End Sub
  25:      End Module
  26:  End Namespace

As you can see, tracing can be turn OFF directly from site config.



And because I use the same line in all methods in any controllers - each controller need to definition a 5 values.


  21:          Private ReadOnly _UserService As IUserService
  22:          Private  _DbForTrace As ApplicationDbContext
  24:          Private ReadOnly _Trace As Boolean
  25:          Private ReadOnly _WithResult As Boolean
  29:          Friend _RequestTraceID As String
  30:   

Because each controller usually has one normal exit with OK result and many error exit (code 500) with various errors I usually trace abnormal exit by attributes. For this purposes I adding to each methods the same attribute.


   1:          <TraceResponse(SaveStatusList:="500")>

If methods can not perform internally and can use only externally (with HttpContext) - in this case we can omit parameter of this attribute. This is code of Attribute.


   1:  Imports System.Reflection
   2:  Imports BackendAPI.Model
   3:  Imports BackendAPI.WebApi.Controllers
   4:  Imports Microsoft.AspNetCore.Mvc
   5:  Imports Microsoft.AspNetCore.Mvc.Filters
   6:  Imports Microsoft.Extensions.Logging
   7:  Imports Microsoft.EntityFrameworkCore
   8:  Imports BackendAPI.Helper
   9:  Imports BackendAPI.Services
  10:  Imports System.Buffers
  11:  Imports System.IO.Pipelines
  12:  Imports System.Text
  13:  Imports Renci.SshNet.Sftp
  14:  Imports Newtonsoft.Json
  15:  Imports Microsoft.AspNetCore.Http
  16:  Imports Microsoft.AspNetCore.Authentication
  17:  Imports System.IO
  18:   
  19:  <AttributeUsage(AttributeTargets.Method)>
  20:  Public Class TraceResponseAttribute
  21:      Inherits ActionFilterAttribute
  22:      ''' <summary>
  23:      ''' "500,200" - default all code by WebRequest is saved to DB (but not return code by directly call API, for directly call use TraceResult)
  24:      ''' </summary>
  25:      Public Property SaveStatusList As String
  26:      Private Property _DB As ApplicationDbContext
  27:      Private Property _Trace As Boolean = False
  28:      Private Property _WithResult As Boolean = False
  29:      Private Property _UserService As IUserService
  30:      Private Property _RequestTraceID As String
  31:      Private Property _StatusList As List(Of Integer)
  32:      Private Property _RetStatusCode As Integer = 0
  33:   
  34:      Public Overrides Sub OnActionExecuted(context As ActionExecutedContext)
  35:          Dim Y As Type = context.Controller.GetType
  36:          For Each OneProp As FieldInfo In Y.GetRuntimeFields
  37:              If OneProp.Name = "_DB" Then
  38:                  _DB = OneProp.GetValue(context.Controller)
  39:              ElseIf OneProp.Name = "_Trace" Then
  40:                  _Trace = OneProp.GetValue(context.Controller)
  41:              ElseIf OneProp.Name = "_WithResult" Then
  42:                  _WithResult = OneProp.GetValue(context.Controller)
  43:              ElseIf OneProp.Name = "_UserService" Then
  44:                  _UserService = OneProp.GetValue(context.Controller)
  45:              ElseIf OneProp.Name = "_RequestTraceID" Then
  46:                  _RequestTraceID = OneProp.GetValue(context.Controller)
  47:              End If
  48:          Next
  49:          If _DB IsNot Nothing And _UserService IsNot Nothing Then
  50:              If _Trace And _WithResult Then
  51:                  If context.Result IsNot Nothing Then
  52:                      If context.Result.GetType.Name = "OkObjectResult" Then
  53:                          _RetStatusCode = CType(context.Result, OkObjectResult).StatusCode
  54:                      ElseIf context.Result.GetType.Name = "JsonResult" Then
  55:                      ElseIf context.Result.GetType.Name = "ObjectResult" Then
  56:                          If CType(context.Result, ObjectResult)?.StatusCode IsNot Nothing Then
  57:                              _RetStatusCode = CType(context.Result, ObjectResult)?.StatusCode
  58:                          End If
  59:                      End If
  60:                  Else
  61:                      _RetStatusCode = 0
  62:                  End If
  63:                  _StatusList = New List(Of Integer)
  64:                  If Not String.IsNullOrEmpty(SaveStatusList) Then
  65:                      Dim Arr1 = SaveStatusList.Split(",")
  66:                      For i As Integer = 0 To Arr1.Count - 1
  67:                          If Not String.IsNullOrEmpty(Arr1(i)) Then
  68:                              _StatusList.Add(CInt(Arr1(i)))
  69:                          End If
  70:                      Next
  71:                  Else
  72:                      GoTo SaveToDB
  73:                  End If
  74:                  Dim CodePresent As Integer = _StatusList.Where(Function(X) X = _RetStatusCode).Count
  75:                  If CodePresent > 0 Then
  76:  SaveToDB:
  77:                      Dim RequestID As String = CType(context.HttpContext, Microsoft.AspNetCore.Http.DefaultHttpContext).TraceIdentifier
  78:                      If String.IsNullOrEmpty(RequestID) Then
  79:                          RequestID = _RequestTraceID
  80:                      End If
  81:                      Dim Result As String = ""
  82:                      If context.Result IsNot Nothing Then
  83:                          If context.Result.GetType.Name = "OkObjectResult" Then
  84:                              Result = JsonConvert.SerializeObject(CType(context.Result, OkObjectResult).Value)
  85:                          ElseIf context.Result.GetType.Name = "JsonResult" Then
  86:                              Result = JsonConvert.SerializeObject(CType(context.Result, JsonResult).Value)
  87:                          ElseIf context.Result.GetType.Name = "ObjectResult" Then
  88:                              Result = JsonConvert.SerializeObject(CType(context.Result, ObjectResult).Value)
  89:                          End If
  90:                      End If
  91:                      _DB = _DB.ReOpen
  92:                      Sql.ExecNonQuery(_DB, $"Update `BackendApiTraceLog` SET Response='{Result.Replace("'", "^")}', ResponseStatus='{_RetStatusCode}' where `TraceID`='{RequestID}';")
  93:                      _DB.Database.GetDbConnection.Close()
  94:                  End If
  95:              End If
  96:          End If
  97:          MyBase.OnActionExecuted(context)
  98:      End Sub
  99:  End Class
 100:   

What main point in this tracing? Main point is _DbContext must be different related to main _Db controller context, in other case you will see a lot of error message from hell.



If you can interesting for other my usually attributes look to this page - My TDD Technique for Backend API development with Xunit (Custom Attribute, WebClient GET/POST, JWT auth, Fact, Theory, InlineData, ClassData iterator function, Inject Log, Txt parsers for console output).

2. Authorization layer.

For understanding this description you can clear understand difference between Authentication and Authorization. Authentication usually made as middle like this https://github.com/Alex-1347/BackendAPI/tree/master/Jwt, look to description of my project template BackendAPI (Net Core 5) project template with Custom Attribute, Service and Controller Example, MySQL database with masked password in config, SwaggerUI, EF Core and extension to EF Core.

Authorization is another operation what define in BackandAPI what exactly operation in Backend authenticated can doing and what exactly operation user can not doing. For example user "Lux@Lux.com" can delete VirtualMachine 1 and and can not delete VirtualMachine 2 - this is Authorization layer.

I use many way to create authorization, for example hardest way with DB and set up right dynamically, middle way for difficulties is role based way (this way like dumb Microsoft developers), but in this page I want to describe more flexible and simple way.

In the beginning of each method I add the same code line.


  50:              If CurUsers.IsAdmin Or Check(Reflection.MethodBase.GetCurrentMethod.Name, Model, _DB) Then
  ...               'Method body
  69:              Else
  70:                  Return Unauthorized()
  71:              End If
  72:          End Function

This is absolute minimal and safe way to authorizing. Than we need to create one simple function Check.



This layers contains a bunch of functions for analyzing request and return true (if API is allow - for example Authenticate API) or false (for example for GetAll API - this API allow only for Admin).

Some API deleted from checking authorization, this means this API allow only for Admin.



3. Special attribute to arrange API for authorization layers.

As you can see in previous screens I aso use special attribute to arrange API to security layers.

   1:  Namespace WebApi.Controllers
   2:      Public Enum SecurityLevel
   3:          Anon = 1
   4:          User = 2
   5:          AdminOnly = 3
   6:          Notification = 4
   7:      End Enum
   8:      <AttributeUsage(AttributeTargets.Method)>
   9:      Public Class SecurityAttribute
  10:          Inherits Attribute
  11:   
  12:          Public ReadOnly Value As SecurityLevel
  13:          Public Sub New(value As SecurityLevel)
  14:              Me.Value = value
  15:          End Sub
  16:   
  17:      End Class
  18:   
  19:  End Namespace

This way allow me ordering API by layers.



For arrange API I use this code.


   1:  Imports System.Reflection
   2:  Imports Xunit
   3:  Imports Xunit.Abstractions
   4:   
   5:  Public Class ProjectState
   6:      Private ReadOnly Log As ITestOutputHelper
   7:   
   8:      Public Sub New(_Log As ITestOutputHelper)
   9:          Log = _Log
  10:      End Sub
  11:   
  12:      <Fact>
  13:      Sub GetApiState()
  14:          Dim AllApi As Integer, OkApi As Integer, NfApi As Integer, AdmApi As Integer, UserApi As Integer, NotifApi As Integer
  15:          Dim API As Assembly = Assembly.LoadFile("G:\Projects\CryptoChestMax\BackendAPI\BackendAPI\bin\Debug\net6.0\BackendAPI.dll")
  16:          Dim Types = API.GetTypes()
  17:          Dim Controllers As List(Of Type) = Types.Where(Function(X) X.Name.Contains("Controller")).ToList
  18:          For i As Integer = 0 To Controllers.Count - 1
  19:              Log.WriteLine($"{i + 1}. {Controllers(i).Name.Replace("Controller", "")}")
  20:              Dim Methods As List(Of MethodInfo) = DirectCast(Controllers(i), System.Reflection.TypeInfo).DeclaredMethods.ToList
  21:              For j As Integer = 0 To Methods.Count - 1
  22:                  Dim Attributes As List(Of CustomAttributeData) = Methods(j).CustomAttributes.ToList
  23:                  Dim StateVal As String
  24:                  Dim SecurityVal As String, SecurityMark As String
  25:                  For k = 0 To Attributes.Count - 1
  26:                      If Attributes(k).AttributeType.Name = "StateAttribute" Then
  27:                          StateVal = Attributes(k).ToString.Replace("[BackendAPI.StateAttribute(", "").Replace(")]", "")
  28:                          AllApi += 1
  29:                          If StateVal.ToLower.Contains("ok.") Then
  30:                              OkApi += 1
  31:                          ElseIf StateVal.ToLower.Contains("api not found") Then
  32:                              NfApi += 1
  33:                          End If
  34:                      ElseIf Attributes(k).AttributeType.Name = "SecurityAttribute" Then
  35:                          '"[BackendAPI.WebApi.Controllers.SecurityAttribute((BackendAPI.WebApi.Controllers.SecurityLevel)1)]"
  36:                          SecurityVal = Attributes(k).ToString.Replace("[BackendAPI.WebApi.Controllers.SecurityAttribute", "").Replace("((BackendAPI.WebApi.Controllers.SecurityLevel)", "").Replace(")]", "")
  37:                          Select Case CInt(SecurityVal)
  38:                              Case 1 : SecurityMark = "(Anon) "
  39:                              Case 2 : SecurityMark = "(User) " : UserApi = UserApi + 1
  40:                              Case 3 : SecurityMark = "(Admin)" : AdmApi = AdmApi + 1
  41:                              Case 4 : SecurityMark = "(Notif)" : NotifApi = NotifApi + 1
  42:                          End Select
  43:                      End If
  44:                  Next
  45:                  Log.WriteLine($"   {SecurityMark} {j + 1}. {Methods(j).Name} : {StateVal}")
  46:              Next
  47:          Next
  48:          Log.WriteLine($"Summary: Total {AllApi}, Finished {OkApi}, NotFound {NfApi}. Admin {AdmApi}, User {UserApi}, NotifApi {NotifApi}.")
  49:      End Sub
  50:  End Class


Comments ( )
Link to this page: http://www.vb-net.com/TraceAndAuthorization/Index.htm
< THANKS ME>