26 changed files with 555 additions and 51 deletions
@ -1,7 +1,33 @@ |
|||
using Volo.Abp.Modularity; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.Reflection; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks; |
|||
|
|||
public class AbpBackgroundTasksAbstractionsModule : AbpModule |
|||
{ |
|||
public override void PreConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
AutoAddJobMonitors(context.Services); |
|||
} |
|||
|
|||
private static void AutoAddJobMonitors(IServiceCollection services) |
|||
{ |
|||
var jobMonitors = new List<Type>(); |
|||
|
|||
services.OnRegistred(context => |
|||
{ |
|||
if (ReflectionHelper.IsAssignableToGenericType(context.ImplementationType, typeof(JobEventBase<>))) |
|||
{ |
|||
jobMonitors.Add(context.ImplementationType); |
|||
} |
|||
}); |
|||
|
|||
services.Configure<AbpBackgroundTasksOptions>(options => |
|||
{ |
|||
options.JobMonitors.AddIfNotContains(jobMonitors); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,44 @@ |
|||
using System; |
|||
using Volo.Abp; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks; |
|||
|
|||
public class AbpJobExecutionException : AbpException |
|||
{ |
|||
public Type JobType { get; } |
|||
/// <summary>
|
|||
/// Creates a new <see cref="AbpJobExecutionException"/> object.
|
|||
/// </summary>
|
|||
/// <param name="innerException">Inner exception</param>
|
|||
public AbpJobExecutionException(Type jobType) |
|||
: this( |
|||
jobType, |
|||
$"Unable to execute job {jobType.Name}.", |
|||
null) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a new <see cref="AbpJobExecutionException"/> object.
|
|||
/// </summary>
|
|||
/// <param name="jobType">Execute job type</param>
|
|||
/// <param name="innerException">Inner exception</param>
|
|||
public AbpJobExecutionException(Type jobType, Exception innerException) |
|||
: this( |
|||
jobType, |
|||
$"Unable to execute job {jobType.Name} because it: {innerException.Message}", |
|||
innerException) |
|||
{ |
|||
} |
|||
/// <summary>
|
|||
/// Creates a new <see cref="AbpJobExecutionException"/> object.
|
|||
/// </summary>
|
|||
/// <param name="jobType">Execute job type</param>
|
|||
/// <param name="message">Exception message</param>
|
|||
/// <param name="innerException">Inner exception</param>
|
|||
public AbpJobExecutionException(Type jobType, string message, Exception innerException) |
|||
: base(message, innerException) |
|||
{ |
|||
JobType = jobType; |
|||
} |
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using System; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks; |
|||
|
|||
public static class JobRunnableContextExtensions |
|||
{ |
|||
public static T GetService<T>(this JobRunnableContext context) |
|||
{ |
|||
return context.ServiceProvider.GetService<T>(); |
|||
} |
|||
|
|||
public static object GetService(this JobRunnableContext context, Type serviceType) |
|||
{ |
|||
return context.ServiceProvider.GetService(serviceType); |
|||
} |
|||
|
|||
public static T GetRequiredService<T>(this JobRunnableContext context) |
|||
{ |
|||
return context.ServiceProvider.GetRequiredService<T>(); |
|||
} |
|||
|
|||
public static object GetRequiredService(this JobRunnableContext context, Type serviceType) |
|||
{ |
|||
return context.ServiceProvider.GetRequiredService(serviceType); |
|||
} |
|||
|
|||
public static string GetString(this JobRunnableContext context, string key) |
|||
{ |
|||
return context.GetJobData(key).ToString(); |
|||
} |
|||
|
|||
public static bool TryGetString(this JobRunnableContext context, string key, out string value) |
|||
{ |
|||
if (context.TryGetJobData(key, out var data) && data != null) |
|||
{ |
|||
value = data.ToString(); |
|||
return true; |
|||
} |
|||
value = default; |
|||
return false; |
|||
} |
|||
|
|||
public static T GetJobData<T>(this JobRunnableContext context, string key) where T : struct |
|||
{ |
|||
var value = context.GetJobData(key); |
|||
|
|||
return value.To<T>(); |
|||
} |
|||
|
|||
public static bool TryGetJobData<T>(this JobRunnableContext context, string key, out T value) where T : struct |
|||
{ |
|||
if (context.TryGetJobData(key, out var data) && data != null) |
|||
{ |
|||
try |
|||
{ |
|||
value = data.To<T>(); |
|||
return true; |
|||
} |
|||
catch |
|||
{ |
|||
} |
|||
} |
|||
value = default; |
|||
return false; |
|||
} |
|||
|
|||
public static object GetJobData(this JobRunnableContext context, string key) |
|||
{ |
|||
if (context.TryGetJobData(key, out var value) && value != null) |
|||
{ |
|||
return value; |
|||
} |
|||
throw new ArgumentException(key + " not specified."); |
|||
} |
|||
|
|||
public static bool TryGetJobData(this JobRunnableContext context, string key, out object value) |
|||
{ |
|||
if (context.JobData.TryGetValue(key, out value)) |
|||
{ |
|||
return true; |
|||
} |
|||
return false; |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,21 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.Emailing" Version="$(VoloAbpPackageVersion)" /> |
|||
<PackageReference Include="Volo.Abp.Sms" Version="$(VoloAbpPackageVersion)" /> |
|||
<PackageReference Include="Volo.Abp.Http.Client" Version="$(VoloAbpPackageVersion)" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\LINGYUN.Abp.BackgroundTasks.Abstractions\LINGYUN.Abp.BackgroundTasks.Abstractions.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,25 @@ |
|||
using Volo.Abp.Emailing; |
|||
using Volo.Abp.Http.Client; |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.Sms; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Jobs; |
|||
|
|||
[DependsOn(typeof(AbpEmailingModule))] |
|||
[DependsOn(typeof(AbpSmsModule))] |
|||
[DependsOn(typeof(AbpHttpClientModule))] |
|||
public class AbpBackgroundTasksJobsModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpBackgroundTasksOptions>(options => |
|||
{ |
|||
options.AddProvider<ConsoleJob>(DefaultJobNames.ConsoleJob); |
|||
options.AddProvider<SendEmailJob>(DefaultJobNames.SendEmailJob); |
|||
options.AddProvider<SendSmsJob>(DefaultJobNames.SendSmsJob); |
|||
options.AddProvider<SleepJob>(DefaultJobNames.SleepJob); |
|||
options.AddProvider<ServiceInvocationJob>(DefaultJobNames.ServiceInvocationJob); |
|||
options.AddProvider<HttpRequestJob>(DefaultJobNames.HttpRequestJob); |
|||
}); |
|||
} |
|||
} |
|||
@ -1,7 +1,7 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Primitives; |
|||
namespace LINGYUN.Abp.BackgroundTasks.Jobs; |
|||
|
|||
public class ConsoleJob : IJobRunnable |
|||
{ |
|||
@ -0,0 +1,29 @@ |
|||
namespace LINGYUN.Abp.BackgroundTasks.Jobs; |
|||
|
|||
public static class DefaultJobNames |
|||
{ |
|||
/// <summary>
|
|||
/// 发送邮件
|
|||
/// </summary>
|
|||
public const string SendEmailJob = "SendEmail"; |
|||
/// <summary>
|
|||
/// 发送短信
|
|||
/// </summary>
|
|||
public const string SendSmsJob = "SendSms"; |
|||
/// <summary>
|
|||
/// 控制台输出
|
|||
/// </summary>
|
|||
public const string ConsoleJob = "Console"; |
|||
/// <summary>
|
|||
/// 休眠
|
|||
/// </summary>
|
|||
public const string SleepJob = "Sleep"; |
|||
/// <summary>
|
|||
/// 服务间调用
|
|||
/// </summary>
|
|||
public const string ServiceInvocationJob = "ServiceInvocation"; |
|||
/// <summary>
|
|||
/// Http请求
|
|||
/// </summary>
|
|||
public const string HttpRequestJob = "HttpRequest"; |
|||
} |
|||
@ -0,0 +1,78 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Net.Http; |
|||
using System.Net.Http.Headers; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Http; |
|||
using Volo.Abp.Json; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Jobs; |
|||
|
|||
public class HttpRequestJob : IJobRunnable |
|||
{ |
|||
public const string PropertyUrl = "url"; |
|||
public const string PropertyMethod = "method"; |
|||
public const string PropertyData = "data"; |
|||
public const string PropertyContentType = "contentType"; |
|||
public const string PropertyHeaders = "headers"; |
|||
public const string PropertyToken = "token"; |
|||
|
|||
public virtual async Task ExecuteAsync(JobRunnableContext context) |
|||
{ |
|||
var clientFactory = context.GetRequiredService<IHttpClientFactory>(); |
|||
|
|||
var client = clientFactory.CreateClient(); |
|||
var requestMessage = BuildRequestMessage(context); |
|||
|
|||
var response = await client.SendAsync( |
|||
requestMessage, |
|||
HttpCompletionOption.ResponseHeadersRead); |
|||
|
|||
var stringContent = await response.Content.ReadAsStringAsync(); |
|||
|
|||
if (!response.IsSuccessStatusCode && stringContent.IsNullOrWhiteSpace()) |
|||
{ |
|||
context.SetResult($"HttpStatusCode: {(int)response.StatusCode}, Reason: {response.ReasonPhrase}"); |
|||
return; |
|||
} |
|||
context.SetResult(stringContent); |
|||
} |
|||
|
|||
protected virtual HttpRequestMessage BuildRequestMessage(JobRunnableContext context) |
|||
{ |
|||
var url = context.GetString(PropertyUrl); |
|||
var method = context.GetString(PropertyMethod); |
|||
context.TryGetJobData(PropertyData, out var data); |
|||
context.TryGetJobData(PropertyContentType, out var contentType); |
|||
|
|||
var jsonSerializer = context.GetRequiredService<IJsonSerializer>(); |
|||
|
|||
var httpRequestMesasge = new HttpRequestMessage(new HttpMethod(method), url); |
|||
if (data != null) |
|||
{ |
|||
// TODO: 需要支持表单类型
|
|||
|
|||
// application/json 支持
|
|||
httpRequestMesasge.Content = new StringContent( |
|||
jsonSerializer.Serialize(data), |
|||
Encoding.UTF8, |
|||
contentType?.ToString() ?? MimeTypes.Application.Json); |
|||
} |
|||
if (context.TryGetJobData(PropertyHeaders, out var headers) && |
|||
headers is IDictionary<string, string> headersDic) |
|||
{ |
|||
foreach (var header in headersDic) |
|||
{ |
|||
httpRequestMesasge.Headers.Add(header.Key, header.Value); |
|||
} |
|||
} |
|||
// TODO: 和 headers 一起?
|
|||
if (context.TryGetString(PropertyToken, out var accessToken)) |
|||
{ |
|||
httpRequestMesasge.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); |
|||
} |
|||
|
|||
return httpRequestMesasge; |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Emailing; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Jobs; |
|||
|
|||
public class SendEmailJob : IJobRunnable |
|||
{ |
|||
public const string PropertyFrom = "from"; |
|||
public const string PropertyTo = "to"; |
|||
public const string PropertySubject = "subject"; |
|||
public const string PropertyBody = "body"; |
|||
public const string PropertyIsBodyHtml = "isBodyHtml"; |
|||
|
|||
public virtual async Task ExecuteAsync(JobRunnableContext context) |
|||
{ |
|||
context.TryGetString(PropertyFrom, out var from); |
|||
var to = context.GetString(PropertyTo); |
|||
var subject = context.GetString(PropertySubject); |
|||
var body = context.GetString(PropertyBody); |
|||
context.TryGetJobData<bool>(PropertyIsBodyHtml, out var isBodyHtml); |
|||
|
|||
var emailSender = context.GetRequiredService<IEmailSender>(); |
|||
|
|||
await emailSender.QueueAsync(from, to, subject, body, isBodyHtml); |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Sms; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Jobs; |
|||
|
|||
public class SendSmsJob : IJobRunnable |
|||
{ |
|||
public const string PropertyPhoneNumber = "phoneNumber"; |
|||
public const string PropertyMessage = "message"; |
|||
public const string PropertyProperties = "properties"; |
|||
|
|||
public virtual async Task ExecuteAsync(JobRunnableContext context) |
|||
{ |
|||
var phoneNumber = context.GetString(PropertyPhoneNumber); |
|||
var message = context.GetString(PropertyMessage); |
|||
|
|||
var smsMessage = new SmsMessage(phoneNumber, message); |
|||
if (context.TryGetJobData(PropertyProperties, out var data) && |
|||
data is IDictionary<string, object> properties) |
|||
{ |
|||
smsMessage.Properties.AddIfNotContains(properties); |
|||
} |
|||
|
|||
var smsSender = context.GetRequiredService<ISmsSender>(); |
|||
|
|||
await smsSender.SendAsync(smsMessage); |
|||
} |
|||
} |
|||
@ -0,0 +1,117 @@ |
|||
using Microsoft.Extensions.Options; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Globalization; |
|||
using System.Linq; |
|||
using System.Reflection; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp; |
|||
using Volo.Abp.Http.Client; |
|||
using Volo.Abp.Http.Client.ClientProxying; |
|||
using Volo.Abp.Http.Client.DynamicProxying; |
|||
using Volo.Abp.Http.Client.Proxying; |
|||
using Volo.Abp.Http.Modeling; |
|||
using Volo.Abp.Localization; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Jobs; |
|||
|
|||
public class ServiceInvocationJob : IJobRunnable |
|||
{ |
|||
public const string PropertyService = "service"; |
|||
public const string PropertyMethod = "method"; |
|||
public const string PropertyCulture = "culture"; |
|||
|
|||
public virtual async Task ExecuteAsync(JobRunnableContext context) |
|||
{ |
|||
// 获取参数列表
|
|||
var type = context.GetString(PropertyService); |
|||
var method = context.GetString(PropertyMethod); |
|||
var serviceType = Type.GetType(type, true); |
|||
var serviceMethod = serviceType.GetMethod(method); |
|||
context.TryGetString(PropertyCulture, out var culture); |
|||
|
|||
using (CultureHelper.Use(culture ?? CultureInfo.CurrentCulture.Name)) |
|||
{ |
|||
// 反射所必须的参数
|
|||
var callRequestMethod = nameof(DynamicHttpProxyInterceptorClientProxy<object>.CallRequestAsync); |
|||
var clientProxyType = typeof(DynamicHttpProxyInterceptorClientProxy<>).MakeGenericType(serviceType); |
|||
var clientProxy = context.GetRequiredService(clientProxyType); |
|||
var clientProxyMethod = typeof(DynamicHttpProxyInterceptorClientProxy<>).GetMethod(callRequestMethod); |
|||
|
|||
// 调用远程服务发现端点
|
|||
var actionApiDescription = await GetActionApiDescriptionModel(context, serviceType, serviceMethod); |
|||
|
|||
// 拼接调用参数
|
|||
var invokeParameters = new Dictionary<string, object>(); |
|||
var methodParameters = serviceMethod.GetParameters(); |
|||
foreach (var parameter in methodParameters) |
|||
{ |
|||
if (context.TryGetJobData(parameter.Name, out var value)) |
|||
{ |
|||
invokeParameters.Add(parameter.Name, value); |
|||
} |
|||
} |
|||
|
|||
// 构造服务代理上下文
|
|||
var clientProxyRequestContext = new ClientProxyRequestContext( |
|||
actionApiDescription, |
|||
invokeParameters, |
|||
serviceType); |
|||
|
|||
if (serviceMethod.ReturnType.GenericTypeArguments.IsNullOrEmpty()) |
|||
{ |
|||
// 直接调用
|
|||
var taskProxy = (Task)clientProxyMethod.Invoke(clientProxy, new object[] { clientProxyRequestContext }); |
|||
await taskProxy; |
|||
} |
|||
else |
|||
{ |
|||
// 有返回值的调用
|
|||
|
|||
var callRequestAsyncMethod = typeof(DynamicHttpProxyInterceptor<object>) |
|||
.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance) |
|||
.First(m => m.Name == callRequestMethod && m.IsGenericMethodDefinition); |
|||
|
|||
var returnType = serviceMethod.ReturnType.GenericTypeArguments[0]; |
|||
var result = (Task)callRequestAsyncMethod |
|||
.MakeGenericMethod(returnType) |
|||
.Invoke(this, new object[] { context }); |
|||
|
|||
context.SetResult(await GetResultAsync(result, returnType)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
protected virtual async Task<ActionApiDescriptionModel> GetActionApiDescriptionModel( |
|||
JobRunnableContext context, |
|||
Type serviceType, |
|||
MethodInfo method) |
|||
{ |
|||
var clientOptions = context.GetRequiredService<IOptions<AbpHttpClientOptions>>().Value; |
|||
var remoteServiceConfigurationProvider = context.GetRequiredService<IRemoteServiceConfigurationProvider>(); |
|||
var proxyHttpClientFactory = context.GetRequiredService<IProxyHttpClientFactory>(); |
|||
var apiDescriptionFinder = context.GetRequiredService<IApiDescriptionFinder>(); |
|||
|
|||
var clientConfig = clientOptions.HttpClientProxies.GetOrDefault(serviceType) ?? |
|||
throw new AbpException($"Could not get DynamicHttpClientProxyConfig for {serviceType.FullName}."); |
|||
var remoteServiceConfig = await remoteServiceConfigurationProvider.GetConfigurationOrDefaultAsync(clientConfig.RemoteServiceName); |
|||
var client = proxyHttpClientFactory.Create(clientConfig.RemoteServiceName); |
|||
|
|||
return await apiDescriptionFinder.FindActionAsync( |
|||
client, |
|||
remoteServiceConfig.BaseUrl, |
|||
serviceType, |
|||
method |
|||
); |
|||
} |
|||
|
|||
protected virtual async Task<object> GetResultAsync(Task task, Type resultType) |
|||
{ |
|||
await task; |
|||
var resultProperty = typeof(Task<>) |
|||
.MakeGenericType(resultType) |
|||
.GetProperty(nameof(Task<object>.Result), BindingFlags.Instance | BindingFlags.Public); |
|||
Check.NotNull(resultProperty, nameof(resultProperty)); |
|||
return resultProperty.GetValue(task); |
|||
} |
|||
} |
|||
@ -1,7 +1,7 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Primitives; |
|||
namespace LINGYUN.Abp.BackgroundTasks.Jobs; |
|||
|
|||
public class SleepJob : IJobRunnable |
|||
{ |
|||
@ -1,14 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Primitives; |
|||
|
|||
public class EmailingJob : IJobRunnable |
|||
{ |
|||
public virtual async Task ExecuteAsync(JobRunnableContext context) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
Loading…
Reference in new issue