Merge pull request #12896 from abpframework/integration-services

Introduce Integration Service Infrastructure
pull/14001/head
maliming 3 years ago committed by GitHub
commit c78497a4ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -43,4 +43,14 @@ public static class ActionDescriptorExtensions
{
return actionDescriptor is PageActionDescriptor;
}
public static PageActionDescriptor AsPageAction(this ActionDescriptor actionDescriptor)
{
if (!actionDescriptor.IsPageAction())
{
throw new AbpException($"{nameof(actionDescriptor)} should be type of {typeof(PageActionDescriptor).AssemblyQualifiedName}");
}
return actionDescriptor as PageActionDescriptor;
}
}

@ -70,7 +70,9 @@ public class AbpAuditActionFilter : IAsyncActionFilter, ITransientDependency
}
var auditingHelper = context.GetRequiredService<IAuditingHelper>();
if (!auditingHelper.ShouldSaveAudit(context.ActionDescriptor.GetMethodInfo(), true))
if (!auditingHelper.ShouldSaveAudit(
context.ActionDescriptor.GetMethodInfo(),
defaultValue: GetDefaultAuditBehavior(options, context.ActionDescriptor)))
{
return false;
}
@ -85,4 +87,20 @@ public class AbpAuditActionFilter : IAsyncActionFilter, ITransientDependency
return true;
}
private static bool GetDefaultAuditBehavior(
AbpAuditingOptions abpAuditingOptions,
ActionDescriptor actionDescriptor)
{
if (!abpAuditingOptions.IsEnabledForIntegrationServices &&
actionDescriptor
.AsControllerActionDescriptor()
.ControllerTypeInfo
.IsDefined(typeof(IntegrationServiceAttribute), true))
{
return false;
}
return true;
}
}

@ -75,7 +75,7 @@ public class AbpAuditPageFilter : IAsyncPageFilter, ITransientDependency
}
var auditingHelper = context.GetRequiredService<IAuditingHelper>();
if (!auditingHelper.ShouldSaveAudit(context.HandlerMethod.MethodInfo, true))
if (!auditingHelper.ShouldSaveAudit(context.HandlerMethod.MethodInfo, defaultValue: true))
{
return false;
}

@ -26,9 +26,11 @@ public class ConventionalRouteBuilder : IConventionalRouteBuilder, ITransientDep
string httpMethod,
[CanBeNull] ConventionalControllerSetting configuration)
{
var controllerNameInUrl = NormalizeUrlControllerName(rootPath, controllerName, action, httpMethod, configuration);
var apiRoutePrefix = GetApiRoutePrefix(action, configuration);
var controllerNameInUrl =
NormalizeUrlControllerName(rootPath, controllerName, action, httpMethod, configuration);
var url = $"api/{rootPath}/{NormalizeControllerNameCase(controllerNameInUrl, configuration)}";
var url = $"{apiRoutePrefix}/{rootPath}/{NormalizeControllerNameCase(controllerNameInUrl, configuration)}";
//Add {id} path if needed
var idParameterModel = action.Parameters.FirstOrDefault(p => p.ParameterName == "id");
@ -69,6 +71,16 @@ public class ConventionalRouteBuilder : IConventionalRouteBuilder, ITransientDep
return url;
}
protected virtual string GetApiRoutePrefix(ActionModel actionModel, ConventionalControllerSetting configuration)
{
if (actionModel.Controller.ControllerType.IsDefined(typeof(IntegrationServiceAttribute), true))
{
return AbpAspNetCoreConsts.DefaultIntegrationServiceApiPrefix;
}
return AbpAspNetCoreConsts.DefaultApiPrefix;
}
protected virtual string NormalizeUrlActionName(string rootPath, string controllerName, ActionModel action,
string httpMethod, [CanBeNull] ConventionalControllerSetting configuration)
{
@ -108,7 +120,8 @@ public class ConventionalRouteBuilder : IConventionalRouteBuilder, ITransientDep
);
}
protected virtual string NormalizeControllerNameCase(string controllerName, [CanBeNull] ConventionalControllerSetting configuration)
protected virtual string NormalizeControllerNameCase(string controllerName,
[CanBeNull] ConventionalControllerSetting configuration)
{
if (configuration?.UseV3UrlStyle ?? Options.UseV3UrlStyle)
{
@ -120,7 +133,8 @@ public class ConventionalRouteBuilder : IConventionalRouteBuilder, ITransientDep
}
}
protected virtual string NormalizeActionNameCase(string actionName, [CanBeNull] ConventionalControllerSetting configuration)
protected virtual string NormalizeActionNameCase(string actionName,
[CanBeNull] ConventionalControllerSetting configuration)
{
if (configuration?.UseV3UrlStyle ?? Options.UseV3UrlStyle)
{
@ -132,13 +146,15 @@ public class ConventionalRouteBuilder : IConventionalRouteBuilder, ITransientDep
}
}
protected virtual string NormalizeIdPropertyNameCase(PropertyInfo property, [CanBeNull] ConventionalControllerSetting configuration)
protected virtual string NormalizeIdPropertyNameCase(PropertyInfo property,
[CanBeNull] ConventionalControllerSetting configuration)
{
return property.Name;
}
protected virtual string NormalizeSecondaryIdNameCase(ParameterModel secondaryId, [CanBeNull] ConventionalControllerSetting configuration)
protected virtual string NormalizeSecondaryIdNameCase(ParameterModel secondaryId,
[CanBeNull] ConventionalControllerSetting configuration)
{
return secondaryId.ParameterName;
}
}
}

@ -0,0 +1,7 @@
namespace Volo.Abp.AspNetCore;
public static class AbpAspNetCoreConsts
{
public const string DefaultApiPrefix = "api";
public const string DefaultIntegrationServiceApiPrefix = "integration-api";
}

@ -10,5 +10,5 @@ public class AbpAspNetCoreAuditingOptions
/// <see cref="AbpAuditingMiddleware"/> will be disabled for URLs
/// starting with an ignored URL.
/// </summary>
public List<string> IgnoredUrls { get; } = new List<string>();
public List<string> IgnoredUrls { get; } = new();
}

@ -94,8 +94,23 @@ public class AbpAuditingMiddleware : IMiddleware, ITransientDependency
private bool IsIgnoredUrl(HttpContext context)
{
return context.Request.Path.Value != null &&
AspNetCoreAuditingOptions.IgnoredUrls.Any(x => context.Request.Path.Value.StartsWith(x));
if (context.Request.Path.Value == null)
{
return false;
}
if (!AuditingOptions.IsEnabledForIntegrationServices &&
context.Request.Path.Value.StartsWith($"/{AbpAspNetCoreConsts.DefaultIntegrationServiceApiPrefix}/"))
{
return true;
}
if (AspNetCoreAuditingOptions.IgnoredUrls.Any(x => context.Request.Path.Value.StartsWith(x)))
{
return true;
}
return false;
}
private async Task<bool> ShouldWriteAuditLogAsync(AuditLogInfo auditLogInfo, HttpContext httpContext, bool hasError)

@ -37,6 +37,12 @@ public class AbpAuditingOptions
/// Default: true.
/// </summary>
public bool AlwaysLogOnException { get; set; }
/// <summary>
/// Disables/enables audit logging for integration services.
/// Default: false.
/// </summary>
public bool IsEnabledForIntegrationServices { get; set; }
public List<Func<AuditLogInfo, Task<bool>>> AlwaysLogSelectors { get; }

@ -52,7 +52,7 @@ public class AuditingHelper : IAuditingHelper, ITransientDependency
CorrelationIdProvider = correlationIdProvider;
}
public virtual bool ShouldSaveAudit(MethodInfo methodInfo, bool defaultValue = false)
public virtual bool ShouldSaveAudit(MethodInfo methodInfo, bool defaultValue = false, bool ignoreIntegrationServiceAttribute = false)
{
if (methodInfo == null)
{
@ -77,7 +77,7 @@ public class AuditingHelper : IAuditingHelper, ITransientDependency
var classType = methodInfo.DeclaringType;
if (classType != null)
{
var shouldAudit = AuditingInterceptorRegistrar.ShouldAuditTypeByDefaultOrNull(classType);
var shouldAudit = AuditingInterceptorRegistrar.ShouldAuditTypeByDefaultOrNull(classType, ignoreIntegrationServiceAttribute);
if (shouldAudit != null)
{
return shouldAudit.Value;

@ -62,7 +62,9 @@ public class AuditingInterceptor : AbpInterceptor, ITransientDependency
return false;
}
if (!auditingHelper.ShouldSaveAudit(invocation.Method))
if (!auditingHelper.ShouldSaveAudit(
invocation.Method,
ignoreIntegrationServiceAttribute: options.IsEnabledForIntegrationServices))
{
return false;
}

@ -22,7 +22,7 @@ public static class AuditingInterceptorRegistrar
return false;
}
if (ShouldAuditTypeByDefaultOrNull(type) == true)
if (ShouldAuditTypeByDefaultOrNull(type, ignoreIntegrationServiceAttribute: true) == true)
{
return true;
}
@ -36,7 +36,7 @@ public static class AuditingInterceptorRegistrar
}
//TODO: Move to a better place
public static bool? ShouldAuditTypeByDefaultOrNull(Type type)
public static bool? ShouldAuditTypeByDefaultOrNull(Type type, bool ignoreIntegrationServiceAttribute)
{
//TODO: In an inheritance chain, it would be better to check the attributes on the top class first.
@ -52,7 +52,10 @@ public static class AuditingInterceptorRegistrar
if (typeof(IAuditingEnabled).IsAssignableFrom(type))
{
return true;
if (ignoreIntegrationServiceAttribute || !type.IsDefined(typeof(IntegrationServiceAttribute), true))
{
return true;
}
}
return null;

@ -7,7 +7,7 @@ namespace Volo.Abp.Auditing;
//TODO: Move ShouldSaveAudit & IsEntityHistoryEnabled and rename to IAuditingFactory
public interface IAuditingHelper
{
bool ShouldSaveAudit(MethodInfo methodInfo, bool defaultValue = false);
bool ShouldSaveAudit(MethodInfo methodInfo, bool defaultValue = false, bool ignoreIntegrationServiceAttribute = false);
bool IsEntityHistoryEnabled(Type entityType, bool defaultValue = false);

@ -1,5 +1,5 @@
namespace Volo.Abp;
public interface IRemoteService //TODO: Can we move this to another package?
public interface IRemoteService
{
}

@ -0,0 +1,9 @@
using System;
namespace Volo.Abp;
[AttributeUsage(AttributeTargets.Class)]
public class IntegrationServiceAttribute : Attribute
{
}

@ -3,8 +3,7 @@
/// <summary>
/// This interface must be implemented by all application services to register and identify them by convention.
/// </summary>
public interface IApplicationService :
IRemoteService
public interface IApplicationService : IRemoteService
{
}
}

@ -1,5 +1,7 @@
using System.Collections.Generic;
using Volo.Abp.Application.Services;
using Volo.Abp.Aspects;
using Volo.Abp.Auditing;
using Volo.Abp.Authorization;
using Volo.Abp.Domain;
using Volo.Abp.Features;
@ -33,10 +35,12 @@ public class AbpDddApplicationModule : AbpModule
{
Configure<AbpApiDescriptionModelOptions>(options =>
{
//TODO: Should we move related items to their own projects?
options.IgnoredInterfaces.AddIfNotContains(typeof(IRemoteService));
options.IgnoredInterfaces.AddIfNotContains(typeof(IRemoteService));
options.IgnoredInterfaces.AddIfNotContains(typeof(IApplicationService));
options.IgnoredInterfaces.AddIfNotContains(typeof(IUnitOfWorkEnabled));
options.IgnoredInterfaces.AddIfNotContains(typeof(IAuditingEnabled));
options.IgnoredInterfaces.AddIfNotContains(typeof(IValidationEnabled));
options.IgnoredInterfaces.AddIfNotContains(typeof(IGlobalFeatureCheckingEnabled));
});
}
}

@ -41,9 +41,9 @@ public abstract class ApplicationService :
[Obsolete("Use LazyServiceProvider instead.")]
public IServiceProvider ServiceProvider { get; set; }
public static string[] CommonPostfixes { get; set; } = { "AppService", "ApplicationService", "Service" };
public static string[] CommonPostfixes { get; set; } = { "AppService", "ApplicationService", "IntService", "IntegrationService", "Service" };
public List<string> AppliedCrossCuttingConcerns { get; } = new List<string>();
public List<string> AppliedCrossCuttingConcerns { get; } = new();
protected IUnitOfWorkManager UnitOfWorkManager => LazyServiceProvider.LazyGetRequiredService<IUnitOfWorkManager>();

@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Mvc;
namespace Volo.Abp.AspNetCore.Mvc.Auditing;
[Route("integration-api/audit-test")]
[IntegrationService]
public class AuditIntegrationServiceTestController : AbpController
{
[HttpGet]
public IActionResult Get()
{
return Ok();
}
}

@ -0,0 +1,58 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using NSubstitute;
using Shouldly;
using Volo.Abp.Auditing;
using Xunit;
namespace Volo.Abp.AspNetCore.Mvc.Auditing;
public class AuditIntegrationServiceTestController_Tests : AspNetCoreMvcTestBase
{
private readonly AbpAuditingOptions _options;
private IAuditingStore _auditingStore;
public AuditIntegrationServiceTestController_Tests()
{
_options = ServiceProvider.GetRequiredService<IOptions<AbpAuditingOptions>>().Value;
_auditingStore = ServiceProvider.GetRequiredService<IAuditingStore>();
}
protected override void ConfigureServices(HostBuilderContext context, IServiceCollection services)
{
_auditingStore = Substitute.For<IAuditingStore>();
services.Replace(ServiceDescriptor.Singleton(_auditingStore));
base.ConfigureServices(context, services);
}
[Fact]
public async Task Should_Write_Audit_Log_For_Controllers_With_IntegrationService_Attribute_If_IsEnabledForIntegrationServices()
{
_options.IsEnabledForGetRequests = true;
_options.IsEnabledForIntegrationServices = true;
await GetResponseAsync("/integration-api/audit-test/");
await _auditingStore
.Received()
.SaveAsync(
Arg.Is<AuditLogInfo>(
x => x.Actions.Any(
a =>
a.MethodName == nameof(AuditIntegrationServiceTestController.Get) &&
a.ServiceName == typeof(AuditIntegrationServiceTestController).FullName
)
)
);
}
[Fact]
public async Task Should_Not_Write_Audit_Log_For_Controllers_With_IntegrationService_Attribute()
{
_options.IsEnabledForGetRequests = true;
await GetResponseAsync("/integration-api/audit-test/");
await _auditingStore.DidNotReceive().SaveAsync(Arg.Any<AuditLogInfo>());
}
}

@ -1,5 +1,4 @@
using System;
using System.Linq;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;

@ -0,0 +1,15 @@
using System.Threading.Tasks;
using Shouldly;
using Xunit;
namespace Volo.Abp.AspNetCore.Mvc;
public class PeopleIntegrationService_Tests : AspNetCoreMvcTestBase
{
[Fact]
public async Task GetValueAsync()
{
var result = await GetResponseAsStringAsync("/integration-api/app/people/value");
result.ShouldBe("42");
}
}

@ -55,6 +55,16 @@ public class Auditing_Tests : AbpAuditingTestBase
await _auditingStore.Received().SaveAsync(Arg.Any<AuditLogInfo>());
}
[Fact]
public async Task Should_Not_Write_AuditLog_For_Classes_With_IntegrationService_Attribute()
{
var myAuditedObject1 = GetRequiredService<MyNotAuditedIntegrationService1>();
await myAuditedObject1.DoItAsync(new InputObject { Value1 = "forty-two", Value2 = 42 });
await _auditingStore.DidNotReceive().SaveAsync(Arg.Any<AuditLogInfo>());
}
public interface IMyAuditedObject : ITransientDependency, IAuditingEnabled
{
@ -72,6 +82,20 @@ public class Auditing_Tests : AbpAuditingTestBase
});
}
}
/* Integration services should not be audited by default */
[IntegrationService]
public class MyNotAuditedIntegrationService1 : IMyAuditedObject
{
public virtual Task<ResultObject> DoItAsync(InputObject inputObject)
{
return Task.FromResult(new ResultObject
{
Value1 = inputObject.Value1 + "-result",
Value2 = inputObject.Value2 + 1
});
}
}
public class ResultObject
{

@ -0,0 +1,9 @@
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace Volo.Abp.TestApp.Application;
public interface IPeopleIntegrationService : IApplicationService
{
Task<string> GetValueAsync();
}

@ -0,0 +1,13 @@
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace Volo.Abp.TestApp.Application;
[IntegrationService]
public class PeopleIntegrationService : ApplicationService, IPeopleIntegrationService
{
public async Task<string> GetValueAsync()
{
return "42";
}
}
Loading…
Cancel
Save