Skip to content

Commit 8e87540

Browse files
committed
add WebHosting middleware
1 parent 15c82b2 commit 8e87540

File tree

8 files changed

+407
-2
lines changed

8 files changed

+407
-2
lines changed

MicroBatchFramework.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".circleci", ".circleci", "{
2424
.circleci\config.yml = .circleci\config.yml
2525
EndProjectSection
2626
EndProject
27+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicroBatchFramework.WebHosting", "src\MicroBatchFramework.WebHosting\MicroBatchFramework.WebHosting.csproj", "{9AC1CAE2-E717-472A-BBFB-0FE5590E5C7A}"
28+
EndProject
29+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebHostingApp", "sandbox\WebHostingApp\WebHostingApp.csproj", "{2B7CDEFC-3D92-4B72-8898-2494D7B087AD}"
30+
EndProject
2731
Global
2832
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2933
Debug|Any CPU = Debug|Any CPU
@@ -50,6 +54,14 @@ Global
5054
{AF15C841-5D45-4E61-BFCE-A6E6B7BA7629}.Debug|Any CPU.Build.0 = Debug|Any CPU
5155
{AF15C841-5D45-4E61-BFCE-A6E6B7BA7629}.Release|Any CPU.ActiveCfg = Release|Any CPU
5256
{AF15C841-5D45-4E61-BFCE-A6E6B7BA7629}.Release|Any CPU.Build.0 = Release|Any CPU
57+
{9AC1CAE2-E717-472A-BBFB-0FE5590E5C7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
58+
{9AC1CAE2-E717-472A-BBFB-0FE5590E5C7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
59+
{9AC1CAE2-E717-472A-BBFB-0FE5590E5C7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
60+
{9AC1CAE2-E717-472A-BBFB-0FE5590E5C7A}.Release|Any CPU.Build.0 = Release|Any CPU
61+
{2B7CDEFC-3D92-4B72-8898-2494D7B087AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
62+
{2B7CDEFC-3D92-4B72-8898-2494D7B087AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
63+
{2B7CDEFC-3D92-4B72-8898-2494D7B087AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
64+
{2B7CDEFC-3D92-4B72-8898-2494D7B087AD}.Release|Any CPU.Build.0 = Release|Any CPU
5365
EndGlobalSection
5466
GlobalSection(SolutionProperties) = preSolution
5567
HideSolutionNode = FALSE
@@ -60,6 +72,8 @@ Global
6072
{5E16EBD8-3396-4952-9B2D-DD2E2E3C883B} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6}
6173
{0B3BE82E-753A-415D-AD4E-90350C6E5C3D} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6}
6274
{AF15C841-5D45-4E61-BFCE-A6E6B7BA7629} = {AAD2D900-C305-4449-A9FC-6C7696FFEDFA}
75+
{9AC1CAE2-E717-472A-BBFB-0FE5590E5C7A} = {1F399F98-7439-4F05-847B-CC1267B4B7F2}
76+
{2B7CDEFC-3D92-4B72-8898-2494D7B087AD} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6}
6377
EndGlobalSection
6478
GlobalSection(ExtensibilityGlobals) = postSolution
6579
SolutionGuid = {7F3E353A-C125-4020-8481-11DC6496358C}

sandbox/WebHostingApp/Program.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using Microsoft.AspNetCore.Hosting;
2+
using Microsoft.Extensions.Logging;
3+
using MicroBatchFramework;
4+
using System;
5+
using System.Threading.Tasks;
6+
7+
namespace WebHostingApp
8+
{
9+
public class Program
10+
{
11+
public static async Task Main(string[] args)
12+
{
13+
await new WebHostBuilder().RunBatchEngineWebHosting("http://localhost:12345");
14+
}
15+
}
16+
17+
public class MyBatch : BatchBase
18+
{
19+
public void Foo()
20+
{
21+
Context.Logger.LogInformation("foo bar baz");
22+
}
23+
24+
public void Sum(int x, int y)
25+
{
26+
Context.Logger.LogInformation((x + y).ToString());
27+
}
28+
29+
public void InOut(string input, Person person)
30+
{
31+
Context.Logger.LogInformation(person.Name + ":" + person.Age);
32+
Context.Logger.LogInformation("In: " + input);
33+
}
34+
35+
public void ErrorMan()
36+
{
37+
throw new Exception("foo bar baz");
38+
}
39+
}
40+
41+
public class Person
42+
{
43+
public int Age { get; set; }
44+
public string Name { get; set; }
45+
}
46+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>netcoreapp2.1</TargetFramework>
6+
</PropertyGroup>
7+
8+
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
9+
<LangVersion>7.3</LangVersion>
10+
</PropertyGroup>
11+
<ItemGroup>
12+
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.0" />
13+
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
14+
</ItemGroup>
15+
<ItemGroup>
16+
<ProjectReference Include="..\..\src\MicroBatchFramework.WebHosting\MicroBatchFramework.WebHosting.csproj" />
17+
<ProjectReference Include="..\..\src\MicroBatchFramework\MicroBatchFramework.csproj" />
18+
</ItemGroup>
19+
</Project>
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
using MicroBatchFramework.WebHosting;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.AspNetCore.Builder;
4+
using Microsoft.AspNetCore.Hosting;
5+
using System.Threading.Tasks;
6+
using System;
7+
using Microsoft.Extensions.Logging;
8+
using System.Reflection;
9+
using System.Collections.Generic;
10+
11+
namespace MicroBatchFramework // .WebHosting
12+
{
13+
public static class BatchEngineHostingExtensions
14+
{
15+
public static IWebHostBuilder PrepareBatchEngineMiddleware(this IWebHostBuilder builder, IBatchInterceptor interceptor = null)
16+
{
17+
var batchTypes = CollectBatchTypes();
18+
var target = new TargetBatchTypeCollection(batchTypes);
19+
20+
return builder
21+
.ConfigureServices(services =>
22+
{
23+
services.AddSingleton<IBatchInterceptor>(interceptor ?? NullBatchInerceptor.Default);
24+
services.AddSingleton<TargetBatchTypeCollection>(target);
25+
foreach (var item in target)
26+
{
27+
services.AddTransient(item);
28+
}
29+
});
30+
}
31+
32+
public static Task RunBatchEngineWebHosting(this IWebHostBuilder builder, string urls, IBatchInterceptor interceptor = null)
33+
{
34+
return builder
35+
.PrepareBatchEngineMiddleware(interceptor)
36+
.UseKestrel()
37+
.UseUrls(urls)
38+
.UseStartup<DefaultStartup>()
39+
.Build()
40+
.RunAsync();
41+
}
42+
43+
public static IApplicationBuilder UseBatchEngineMiddleware(this IApplicationBuilder builder)
44+
{
45+
return builder.UseMiddleware<BatchEngineMiddleware>();
46+
}
47+
48+
public class DefaultStartup
49+
{
50+
public void Configure(IApplicationBuilder app, IApplicationLifetime lifetime)
51+
{
52+
var interceptor = app.ApplicationServices.GetService<IBatchInterceptor>();
53+
var provider = app.ApplicationServices.GetService<IServiceProvider>();
54+
var logger = app.ApplicationServices.GetService<ILogger<BatchEngine>>();
55+
56+
lifetime.ApplicationStarted.Register(async () =>
57+
{
58+
try
59+
{
60+
await interceptor.OnBatchEngineBeginAsync(provider, logger);
61+
}
62+
catch { }
63+
});
64+
65+
lifetime.ApplicationStopped.Register(async () =>
66+
{
67+
try
68+
{
69+
await interceptor.OnBatchEngineEndAsync();
70+
}
71+
catch { }
72+
});
73+
74+
app.UseBatchEngineMiddleware();
75+
}
76+
}
77+
78+
static List<Type> CollectBatchTypes()
79+
{
80+
List<Type> batchBaseTypes = new List<Type>();
81+
82+
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
83+
{
84+
if (asm.FullName.StartsWith("System") || asm.FullName.StartsWith("Microsoft.Extensions")) continue;
85+
86+
Type[] types;
87+
try
88+
{
89+
types = asm.GetTypes();
90+
}
91+
catch (ReflectionTypeLoadException ex)
92+
{
93+
types = ex.Types;
94+
}
95+
96+
foreach (var item in types)
97+
{
98+
if (typeof(BatchBase).IsAssignableFrom(item) && item != typeof(BatchBase))
99+
{
100+
batchBaseTypes.Add(item);
101+
}
102+
}
103+
}
104+
105+
return batchBaseTypes;
106+
}
107+
}
108+
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
using Microsoft.AspNetCore.Http;
2+
using Microsoft.Extensions.Logging;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Net;
6+
using System.Reflection;
7+
using System.Text;
8+
using System.Threading.Tasks;
9+
10+
namespace MicroBatchFramework.WebHosting
11+
{
12+
internal class WebHostingInterceptor : IBatchInterceptor
13+
{
14+
readonly IBatchInterceptor innerInterceptor;
15+
16+
public bool CompleteSuccessfully { get; private set; }
17+
public string ErrorMessage { get; private set; }
18+
public Exception Exception { get; private set; }
19+
20+
public WebHostingInterceptor(IBatchInterceptor innerInterceptor)
21+
{
22+
this.innerInterceptor = innerInterceptor;
23+
}
24+
25+
public ValueTask OnBatchEngineBeginAsync(IServiceProvider serviceProvider, ILogger<BatchEngine> logger)
26+
{
27+
return innerInterceptor.OnBatchEngineBeginAsync(serviceProvider, logger);
28+
}
29+
30+
public ValueTask OnBatchEngineEndAsync()
31+
{
32+
return innerInterceptor.OnBatchEngineEndAsync();
33+
}
34+
35+
public ValueTask OnBatchRunBeginAsync(BatchContext context)
36+
{
37+
return innerInterceptor.OnBatchRunBeginAsync(context);
38+
}
39+
40+
public ValueTask OnBatchRunCompleteAsync(BatchContext context, string errorMessageIfFailed, Exception exceptionIfExists)
41+
{
42+
this.CompleteSuccessfully = (errorMessageIfFailed == null && exceptionIfExists == null);
43+
this.ErrorMessage = errorMessageIfFailed;
44+
this.Exception = exceptionIfExists;
45+
return innerInterceptor.OnBatchRunCompleteAsync(context, errorMessageIfFailed, exceptionIfExists);
46+
}
47+
}
48+
49+
internal class LogCollector : ILogger<BatchEngine>
50+
{
51+
readonly ILogger<BatchEngine> innerLogger;
52+
readonly StringBuilder sb;
53+
54+
public LogCollector(ILogger<BatchEngine> innerLogger)
55+
{
56+
this.innerLogger = innerLogger;
57+
this.sb = new StringBuilder();
58+
}
59+
60+
public IDisposable BeginScope<TState>(TState state)
61+
{
62+
return innerLogger.BeginScope(state);
63+
}
64+
65+
public bool IsEnabled(LogLevel logLevel)
66+
{
67+
return true;
68+
}
69+
70+
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
71+
{
72+
var msg = formatter(state, exception);
73+
lock (sb)
74+
{
75+
sb.AppendLine(msg);
76+
}
77+
78+
innerLogger.Log<TState>(logLevel, eventId, state, exception, formatter);
79+
}
80+
81+
public override string ToString()
82+
{
83+
lock (sb)
84+
{
85+
return sb.ToString();
86+
}
87+
}
88+
}
89+
90+
public class BatchEngineMiddleware
91+
{
92+
readonly RequestDelegate next;
93+
readonly IServiceProvider provider;
94+
readonly ILogger<BatchEngine> logger;
95+
readonly IBatchInterceptor interceptor;
96+
97+
readonly Dictionary<string, MethodInfo> methodLookup;
98+
99+
public BatchEngineMiddleware(RequestDelegate next, ILogger<BatchEngine> logger, IBatchInterceptor interceptor, IServiceProvider provider, TargetBatchTypeCollection targetTypes)
100+
{
101+
this.next = next;
102+
this.logger = logger;
103+
this.interceptor = interceptor;
104+
this.provider = provider;
105+
this.methodLookup = BuildMethodLookup(targetTypes);
106+
}
107+
108+
public async Task Invoke(HttpContext httpContext)
109+
{
110+
var path = httpContext.Request.Path.Value;
111+
if (!methodLookup.TryGetValue(path, out var methodInfo))
112+
{
113+
await next(httpContext);
114+
return;
115+
}
116+
117+
// create args
118+
string[] args = null;
119+
try
120+
{
121+
args = new string[(httpContext.Request.Form.Count * 2) + 1];
122+
{
123+
var i = 0;
124+
args[i++] = methodInfo.DeclaringType.Name + "." + methodInfo.Name;
125+
foreach (var item in httpContext.Request.Form)
126+
{
127+
args[i++] = "-" + item.Key;
128+
args[i++] = (item.Value.Count == 0) ? null : item.Value[0];
129+
}
130+
}
131+
}
132+
catch (Exception ex)
133+
{
134+
httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
135+
await httpContext.Response.WriteAsync(ex.ToString());
136+
return;
137+
}
138+
139+
// run with collect statuses
140+
var hostingInterceptor = new WebHostingInterceptor(interceptor);
141+
var collectLogger = new LogCollector(logger);
142+
143+
var engine = new BatchEngine(collectLogger, provider, hostingInterceptor, httpContext.RequestAborted);
144+
await engine.RunAsync(methodInfo.DeclaringType, methodInfo, args);
145+
146+
// out result
147+
if (hostingInterceptor.CompleteSuccessfully)
148+
{
149+
httpContext.Response.StatusCode = (int)HttpStatusCode.OK;
150+
await httpContext.Response.WriteAsync(collectLogger.ToString());
151+
}
152+
else
153+
{
154+
var errorMsg = ((hostingInterceptor.ErrorMessage != null) ? hostingInterceptor.ErrorMessage + Environment.NewLine : "")
155+
+ ((hostingInterceptor.Exception != null) ? hostingInterceptor.Exception.ToString() : "");
156+
httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
157+
await httpContext.Response.WriteAsync(errorMsg);
158+
}
159+
}
160+
161+
static Dictionary<string, MethodInfo> BuildMethodLookup(IEnumerable<Type> batchTypes)
162+
{
163+
var methods = new Dictionary<string, MethodInfo>();
164+
165+
foreach (var type in batchTypes)
166+
{
167+
foreach (var item in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
168+
{
169+
methods.Add("/" + type.Name + "/" + item.Name, item);
170+
}
171+
}
172+
173+
return methods;
174+
}
175+
}
176+
}

0 commit comments

Comments
 (0)