diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Microsoft/AspNetCore/Mvc/Abstractions/ActionDescriptorExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Microsoft/AspNetCore/Mvc/Abstractions/ActionDescriptorExtensions.cs index fb992cb4af..97b5a6c61b 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Microsoft/AspNetCore/Mvc/Abstractions/ActionDescriptorExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Microsoft/AspNetCore/Mvc/Abstractions/ActionDescriptorExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Reflection; using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.RazorPages; using Volo.Abp; using Volo.Abp.AspNetCore.Mvc; @@ -37,5 +38,35 @@ namespace Microsoft.AspNetCore.Mvc.Abstractions { return actionDescriptor is ControllerActionDescriptor; } + + public static PageActionDescriptor AsPageActionDescriptor(this ActionDescriptor actionDescriptor) + { + if (!actionDescriptor.IsPageAction()) + { + throw new AbpException($"{nameof(actionDescriptor)} should be type of {typeof(PageActionDescriptor).AssemblyQualifiedName}"); + } + + return actionDescriptor as PageActionDescriptor; + } + + public static MethodInfo GetPageActionMethodInfo(this ActionDescriptor actionDescriptor) + { + return actionDescriptor.AsPageActionDescriptor().GetMethodInfo(); + } + + public static Type GetPageActionReturnType(this PageActionDescriptor actionDescriptor) + { + return actionDescriptor.GetPageActionMethodInfo().ReturnType; + } + + public static bool HasObjectResult(this PageActionDescriptor actionDescriptor) + { + return ActionResultHelper.IsObjectResult(actionDescriptor.GetReturnType()); + } + + public static bool IsPageAction(this ActionDescriptor actionDescriptor) + { + return actionDescriptor is PageActionDescriptor; + } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs index 4aedf0b087..64d9daca93 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs @@ -16,7 +16,8 @@ namespace Volo.Abp.AspNetCore.Mvc public static void AddAbp(this MvcOptions options, IServiceCollection services) { AddConventions(options, services); - AddFilters(options); + AddActionFilters(options); + AddPageFilters(options); AddModelBinders(options); AddMetadataProviders(options, services); } @@ -26,7 +27,7 @@ namespace Volo.Abp.AspNetCore.Mvc options.Conventions.Add(new AbpServiceConventionWrapper(services)); } - private static void AddFilters(MvcOptions options) + private static void AddActionFilters(MvcOptions options) { options.Filters.AddService(typeof(AbpAuditActionFilter)); options.Filters.AddService(typeof(AbpNoContentActionFilter)); @@ -36,6 +37,14 @@ namespace Volo.Abp.AspNetCore.Mvc options.Filters.AddService(typeof(AbpExceptionFilter)); } + private static void AddPageFilters(MvcOptions options) + { + options.Filters.AddService(typeof(AbpAuditPageFilter)); + options.Filters.AddService(typeof(AbpFeaturePageFilter)); + options.Filters.AddService(typeof(AbpUowPageFilter)); + options.Filters.AddService(typeof(AbpExceptionPageFilter)); + } + private static void AddModelBinders(MvcOptions options) { options.ModelBinderProviders.Insert(0, new AbpDateTimeModelBinderProvider()); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Auditing/AbpAuditPageFilter.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Auditing/AbpAuditPageFilter.cs new file mode 100644 index 0000000000..23e94790e9 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Auditing/AbpAuditPageFilter.cs @@ -0,0 +1,103 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Options; +using Volo.Abp.Aspects; +using Volo.Abp.Auditing; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AspNetCore.Mvc.Auditing +{ + public class AbpAuditPageFilter : IAsyncPageFilter, ITransientDependency + { + protected AbpAuditingOptions Options { get; } + private readonly IAuditingHelper _auditingHelper; + private readonly IAuditingManager _auditingManager; + + public AbpAuditPageFilter(IOptions options, IAuditingHelper auditingHelper, IAuditingManager auditingManager) + { + Options = options.Value; + _auditingHelper = auditingHelper; + _auditingManager = auditingManager; + } + + public Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context) + { + return Task.CompletedTask; + } + + public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next) + { + if (context.HandlerMethod == null || !ShouldSaveAudit(context, out var auditLog, out var auditLogAction)) + { + await next(); + return; + } + + using (AbpCrossCuttingConcerns.Applying(context.HandlerInstance, AbpCrossCuttingConcerns.Auditing)) + { + var stopwatch = Stopwatch.StartNew(); + + try + { + var result = await next(); + + if (result.Exception != null && !result.ExceptionHandled) + { + auditLog.Exceptions.Add(result.Exception); + } + } + catch (Exception ex) + { + auditLog.Exceptions.Add(ex); + throw; + } + finally + { + stopwatch.Stop(); + auditLogAction.ExecutionDuration = Convert.ToInt32(stopwatch.Elapsed.TotalMilliseconds); + auditLog.Actions.Add(auditLogAction); + } + } + } + + private bool ShouldSaveAudit(PageHandlerExecutingContext context, out AuditLogInfo auditLog, out AuditLogActionInfo auditLogAction) + { + auditLog = null; + auditLogAction = null; + + if (!Options.IsEnabled) + { + return false; + } + + if (!context.ActionDescriptor.IsPageAction()) + { + return false; + } + + var auditLogScope = _auditingManager.Current; + if (auditLogScope == null) + { + return false; + } + + if (!_auditingHelper.ShouldSaveAudit(context.ActionDescriptor.GetMethodInfo(), true)) + { + return false; + } + + auditLog = auditLogScope.Log; + auditLogAction = _auditingHelper.CreateAuditLogAction( + auditLog, + context.ActionDescriptor.AsControllerActionDescriptor().ControllerTypeInfo.AsType(), + context.ActionDescriptor.AsControllerActionDescriptor().MethodInfo, + context.HandlerArguments + ); + + return true; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/AbpExceptionPageFilter.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/AbpExceptionPageFilter.cs new file mode 100644 index 0000000000..8aa45688ca --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/AbpExceptionPageFilter.cs @@ -0,0 +1,112 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Volo.Abp.AspNetCore.ExceptionHandling; +using Volo.Abp.DependencyInjection; +using Volo.Abp.ExceptionHandling; +using Volo.Abp.Http; +using Volo.Abp.Json; + +namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling +{ + public class AbpExceptionPageFilter : IAsyncPageFilter, ITransientDependency + { + public ILogger Logger { get; set; } + + private readonly IExceptionToErrorInfoConverter _errorInfoConverter; + private readonly IHttpExceptionStatusCodeFinder _statusCodeFinder; + private readonly IJsonSerializer _jsonSerializer; + + public AbpExceptionPageFilter( + IExceptionToErrorInfoConverter errorInfoConverter, + IHttpExceptionStatusCodeFinder statusCodeFinder, + IJsonSerializer jsonSerializer) + { + _errorInfoConverter = errorInfoConverter; + _statusCodeFinder = statusCodeFinder; + _jsonSerializer = jsonSerializer; + + Logger = NullLogger.Instance; + } + + + public Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context) + { + return Task.CompletedTask; + } + + public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next) + { + if (context.HandlerMethod == null || !ShouldHandleException(context)) + { + await next(); + return; + } + + var pageHandlerExecutedContext = await next(); + if (pageHandlerExecutedContext.Exception == null) + { + return;; + } + + await HandleAndWrapException(pageHandlerExecutedContext); + } + + protected virtual bool ShouldHandleException(PageHandlerExecutingContext context) + { + //TODO: Create DontWrap attribute to control wrapping..? + + if (context.ActionDescriptor.IsPageAction() && + context.ActionDescriptor.HasObjectResult()) + { + return true; + } + + if (context.HttpContext.Request.CanAccept(MimeTypes.Application.Json)) + { + return true; + } + + if (context.HttpContext.Request.IsAjax()) + { + return true; + } + + return false; + } + + protected virtual async Task HandleAndWrapException(PageHandlerExecutedContext context) + { + //TODO: Trigger an AbpExceptionHandled event or something like that. + + context.HttpContext.Response.Headers.Add(AbpHttpConsts.AbpErrorFormat, "true"); + context.HttpContext.Response.StatusCode = (int)_statusCodeFinder.GetStatusCode(context.HttpContext, context.Exception); + + var remoteServiceErrorInfo = _errorInfoConverter.Convert(context.Exception); + + context.Result = new ObjectResult(new RemoteServiceErrorResponse(remoteServiceErrorInfo)); + + var logLevel = context.Exception.GetLogLevel(); + + Logger.LogWithLevel(logLevel, $"---------- {nameof(RemoteServiceErrorInfo)} ----------"); + Logger.LogWithLevel(logLevel, _jsonSerializer.Serialize(remoteServiceErrorInfo, indented: true)); + Logger.LogException(context.Exception, logLevel); + + await context.HttpContext + .RequestServices + .GetRequiredService() + .NotifyAsync( + new ExceptionNotificationContext(context.Exception) + ); + + context.Exception = null; //Handled! + } + + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Features/AbpFeaturePageFilter.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Features/AbpFeaturePageFilter.cs new file mode 100644 index 0000000000..ceb117d2ff --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Features/AbpFeaturePageFilter.cs @@ -0,0 +1,44 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Volo.Abp.Aspects; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Features; + +namespace Volo.Abp.AspNetCore.Mvc.Features +{ + public class AbpFeaturePageFilter : IAsyncPageFilter, ITransientDependency + { + private readonly IMethodInvocationFeatureCheckerService _methodInvocationAuthorizationService; + + public AbpFeaturePageFilter(IMethodInvocationFeatureCheckerService methodInvocationAuthorizationService) + { + _methodInvocationAuthorizationService = methodInvocationAuthorizationService; + } + + public Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context) + { + return Task.CompletedTask; + } + + public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next) + { + if (context.HandlerMethod == null || !context.ActionDescriptor.IsPageAction()) + { + await next(); + return; + } + + var methodInfo = context.ActionDescriptor.GetMethodInfo(); + + using (AbpCrossCuttingConcerns.Applying(context.HandlerInstance, AbpCrossCuttingConcerns.FeatureChecking)) + { + await _methodInvocationAuthorizationService.CheckAsync( + new MethodInvocationFeatureCheckerContext(methodInfo) + ); + + await next(); + } + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowPageFilter.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowPageFilter.cs new file mode 100644 index 0000000000..aeed982173 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowPageFilter.cs @@ -0,0 +1,105 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Options; +using Volo.Abp.AspNetCore.Uow; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Uow; + +namespace Volo.Abp.AspNetCore.Mvc.Uow +{ + public class AbpUowPageFilter : IAsyncPageFilter, ITransientDependency + { + private readonly IUnitOfWorkManager _unitOfWorkManager; + private readonly AbpUnitOfWorkDefaultOptions _defaultOptions; + + public AbpUowPageFilter(IUnitOfWorkManager unitOfWorkManager, IOptions options) + { + _unitOfWorkManager = unitOfWorkManager; + _defaultOptions = options.Value; + } + public Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context) + { + return Task.CompletedTask; + } + + public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next) + { + if (context.HandlerMethod == null || !context.ActionDescriptor.IsPageAction()) + { + await next(); + return; + } + + var methodInfo = context.ActionDescriptor.GetMethodInfo(); + var unitOfWorkAttr = UnitOfWorkHelper.GetUnitOfWorkAttributeOrNull(methodInfo); + + context.HttpContext.Items["_AbpActionInfo"] = new AbpActionInfoInHttpContext + { + IsObjectResult = context.ActionDescriptor.HasObjectResult() + }; + + if (unitOfWorkAttr?.IsDisabled == true) + { + await next(); + return; + } + + var options = CreateOptions(context, unitOfWorkAttr); + + //Trying to begin a reserved UOW by AbpUnitOfWorkMiddleware + if (_unitOfWorkManager.TryBeginReserved(AbpUnitOfWorkMiddleware.UnitOfWorkReservationName, options)) + { + var result = await next(); + if (!Succeed(result)) + { + await RollbackAsync(context); + } + + return; + } + + //Begin a new, independent unit of work + using (var uow = _unitOfWorkManager.Begin(options)) + { + var result = await next(); + if (Succeed(result)) + { + await uow.CompleteAsync(context.HttpContext.RequestAborted); + } + } + } + + private AbpUnitOfWorkOptions CreateOptions(PageHandlerExecutingContext context, UnitOfWorkAttribute unitOfWorkAttribute) + { + var options = new AbpUnitOfWorkOptions(); + + unitOfWorkAttribute?.SetOptions(options); + + if (unitOfWorkAttribute?.IsTransactional == null) + { + options.IsTransactional = _defaultOptions.CalculateIsTransactional( + autoValue: !string.Equals(context.HttpContext.Request.Method, HttpMethod.Get.Method, StringComparison.OrdinalIgnoreCase) + ); + } + + return options; + } + + private async Task RollbackAsync(PageHandlerExecutingContext context) + { + var currentUow = _unitOfWorkManager.Current; + if (currentUow != null) + { + await currentUow.RollbackAsync(context.HttpContext.RequestAborted); + } + } + + private static bool Succeed(PageHandlerExecutedContext result) + { + return result.Exception == null || result.ExceptionHandled; + } + } +} \ No newline at end of file