MicroBatchFramework is an infrastructure of creating CLI(Command-line interface) tools, daemon, and multiple contained batch program. Easy to bind argument to the simple method definition. It built on .NET Generic Host so you can configure Configuration, Logging, DI, etc can load by the standard way.
NuGet: MicroBatchFramework
Install-Package MicroBatchFramework
CLI Tools can write by simple method, argument is automatically binded to parameter.
using MicroBatchFramework;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
// Entrypoint, create from the .NET Core Console App.
class Program
{
// C# 7.1(update lang version)
static async Task Main(string[] args)
{
// you can use new HostBuilder() instead of CreateDefaultBuilder
await BatchHost.CreateDefaultBuilder().RunBatchEngineAsync<MyFirstBatch>(args);
}
}
// Batch definition.
public class MyFirstBatch : BatchBase // inherit BatchBase
{
// allows void/Task return type, parameter allows all types(deserialized by Utf8Json and can pass by JSON string)
public void Hello(string name, int repeat = 3)
{
for (int i = 0; i < repeat; i++)
{
this.Context.Logger.LogInformation($"Hello My Batch from {name}");
}
}
}
You can execute command like SampleApp.exe -name "foo" -repeat 5
.
The Option parser is no longer needed. You can also use the OptionAttribute
to describe the parameter.
public void Hello(
[Option("n", "name of send user.")]string name,
[Option("r", "repeat count.")]int repeat = 3)
{
help
command shows there detail.
> SampleApp.exe help
-n, -name: name of send user.
-r, -repeat: [default=3]repeat count.
You can use CommandAttribute
to create multi command program.
public class MyFirstBatch : BatchBase
{
public void Hello(
[Option("n", "name of send user.")]string name,
[Option("r", "repeat count.")]int repeat = 3)
{
for (int i = 0; i < repeat; i++)
{
this.Context.Logger.LogInformation($"Hello My Batch from {name}");
}
}
[Command("version")]
public void ShowVersion()
{
var version = Assembly.GetExecutingAssembly()
.GetCustomAttribute<AssemblyFileVersionAttribute>()
.Version;
Console.WriteLine(version);
}
// [Option(int)] describes that parameter is passed by index
[Command("escape")]
public void UrlEscape([Option(0)]string input)
{
Console.WriteLine(Uri.EscapeDataString(input));
}
[Command("timer")]
public async Task Timer([Option(0)]uint waitSeconds)
{
Console.WriteLine(waitSeconds + " seconds");
while (waitSeconds != 0)
{
// MicroBatchFramework does not stop immediately on terminate command(Ctrl+C)
// so you have to pass Context.CancellationToken to async method.
await Task.Delay(TimeSpan.FromSeconds(1), Context.CancellationToken);
waitSeconds--;
Console.WriteLine(waitSeconds + " seconds");
}
}
}
You can call like
SampleApp.exe -n "foo" -r 3
SampleApp.exe version
SampleApp.exe escape http://foo.bar/
SampleApp.exe timer 10
MicroBatchFramework allows the multi contained batch. You can write many class, methods and select by first-argument.
using MicroBatchFramework;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
// Entrypoint.
class Program
{
static async Task Main(string[] args)
{
await BatchHost.CreateDefaultBuilder().RunBatchEngineAsync(args); // don't pass <T>.
}
}
// Batches.
public class Foo : BatchBase
{
public void Echo(string msg)
{
this.Context.Logger.LogInformation(msg);
}
public void Sum(int x, int y)
{
this.Context.Logger.LogInformation((x + y).ToString());
}
}
public class Bar : BatchBase
{
public void Hello2()
{
this.Context.Logger.LogInformation("H E L L O");
}
}
You can call {TypeName}.{MethodName}
like
SampleApp.exe Foo.Echo -msg "aaaaa"
SampleApp.exe Foo.Sum -x 100 -y 200
SampleApp.exe Bar.Hello2
list
command shows all invokable methods.
> SampleApp.exe list
Foo.Echo
Foo.Sum
Bar.Hello2
also use with help
> SampleApp.exe help Foo.Echo
-msg: String
BatchBase(this).Context.CancellationToken
is lifecycle token of batch. In default, MicroBatchFramework does not abort on received terminate request, you can check CancellationToken.IsCancellationRequested
and shutdown gracefully. If use infinite-loop, it becomes daemon program.
public class Daemon : BatchBase
{
public async Task Run()
{
// you can write infinite-loop while stop request(Ctrl+C or docker terminate).
try
{
while (!Context.CancellationToken.IsCancellationRequested)
{
try
{
Context.Logger.LogDebug("Wait One Minutes");
}
catch (Exception ex)
{
// error occured but continue to run(or terminate).
Context.Logger.LogError(ex, "Found error");
}
// wait for next time
await Task.Delay(TimeSpan.FromMinutes(1), Context.CancellationToken);
}
}
catch (Exception ex) when (!(ex is OperationCanceledException))
{
// you can write finally exception handling(without cancellation)
}
finally
{
// you can write cleanup code here.
}
}
}
Interceptor can hook before/after batch running event. You can imprement IBatchInterceptor
for it.
BatchContext.Timestamp
has start time so if subtraction from now, get elapsed time.
public class LogRunningTimeInterceptor : IBatchInterceptor
{
public ValueTask OnBatchEngineBeginAsync(IServiceProvider serviceProvider, ILogger<BatchEngine> logger)
{
return default;
}
public ValueTask OnBatchEngineEndAsync()
{
return default;
}
public ValueTask OnBatchRunBeginAsync(BatchContext context)
{
context.Logger.LogInformation("Batch Begin at " + context.Timestamp.ToLocalTime()); // LocalTime for human readable time
return default;
}
public ValueTask OnBatchRunCompleteAsync(BatchContext context, string errorMessageIfFailed, Exception exceptionIfExists)
{
context.Logger.LogInformation("Batch Completed, Elapsed:" + (DateTimeOffset.UtcNow - context.Timestamp));
return default;
}
}
In default, MicroBatchFramework does not prevent double startup but if create interceptor, can do.
public class MutexInterceptor : IBatchInterceptor
{
Mutex mutex;
bool hasHandle = false;
public ValueTask OnBatchEngineBeginAsync(IServiceProvider serviceProvider, ILogger<BatchEngine> logger)
{
mutex = new Mutex(false, Assembly.GetEntryAssembly().GetName().Name);
if (!mutex.WaitOne(0, false))
{
hasHandle = true;
throw new Exception("already running another process.");
}
return default;
}
public ValueTask OnBatchEngineEndAsync()
{
if (hasHandle)
{
mutex.ReleaseMutex();
}
mutex.Dispose();
return default;
}
public ValueTask OnBatchRunBeginAsync(BatchContext context)
{
return default;
}
public ValueTask OnBatchRunCompleteAsync(BatchContext context, string errorMessageIfFailed, Exception exceptionIfExists)
{
return default;
}
}
There interceptor can pass to startup.
class Program
{
static async Task Main(string[] args)
{
await BatchHost.CreateDefaultBuilder()
.RunBatchEngineAsync(args, new LogRunningTimeInterceptor());
}
}
If you want to use multiple interceptor, you can use CompositeBatchInterceptor
.
class Program
{
static async Task Main(string[] args)
{
await BatchHost.CreateDefaultBuilder()
.RunBatchEngineAsync(args, new CompositeBatchInterceptor
{
new LogRunningTimeInterceptor(),
new MutexInterceptor()
});
}
}
MicroBatchFramework is just a infrastructure. You can add appsettings.json or other configs as .NET Core offers via ConfigureAppConfiguration
.
You can add appsettings.json
and appsettings.<env>.json
and typesafe load via map config to Class w/IOption.
Here's single contained batch with Config loading sample.
// appconfig.json(Content, Copy to Output Directory)
{
"Foo": 42,
"Bar": true
}
class Program
{
static async Task Main(string[] args)
{
await BatchHost.CreateDefaultBuilder()
.ConfigureServices((hostContext, services) =>
{
// mapping config json to IOption<MyConfig>
services.Configure<MyConfig>(hostContext.Configuration);
})
.RunBatchEngineAsync<MyFirstBatch>(args);
}
}
public class MyFirstBatch : BatchBase
{
IOptions<MyConfig> config;
// get configuration from DI.
public MyFirstBatch(IOptions<MyConfig> config)
{
this.config = config;
}
public void ShowOption()
{
Console.WriteLine(config.Value.Bar);
Console.WriteLine(config.Value.Foo);
}
}
BatchHost.CreateDefaultBuilder()
is similar as WebHost.CreateDefaultBuilder
on ASP.NET Core, that setup like below.
var builder = new HostBuilder();
// set the content root to executing assembly's location.
builder.UseContentRoot(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
builder.ConfigureAppConfiguration((hostingContext, config) =>
{
var env = hostingContext.HostingEnvironment;
// Get/Set Environement Name.
env.ApplicationName = Assembly.GetExecutingAssembly().GetName().Name;
if (string.IsNullOrWhiteSpace(contextEnvironmentVariable))
{
contextEnvironmentVariable = "NETCORE_ENVIRONMENT";
}
env.EnvironmentName = System.Environment.GetEnvironmentVariable(contextEnvironmentVariable) ?? "Production";
// Load settings from JSON file.
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
config.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
// If EnvironmentName is "Development", try to load UserSecrets.
if (env.IsDevelopment())
{
var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
if (appAssembly != null)
{
config.AddUserSecrets(appAssembly, optional: true);
}
}
// Load settings from Environment variables.
config.AddEnvironmentVariables();
});
builder.ConfigureLogging(logging =>
{
// if embeded SimpleConsoleLogger(default is true), setup logging(MinLogLevel's default is Debug).
if (useSimpleConosoleLogger)
{
builder.ConfigureLogging(logging =>
{
logging.AddSimpleConsole();
logging.AddFilter<SimpleConsoleLoggerProvider>((category, level) =>
{
// omit system message
if (category.StartsWith("Microsoft.Extensions.Hosting.Internal"))
{
if (level <= LogLevel.Debug) return false;
}
return level >= minSimpleConsoleLoggerLogLevel;
});
});
}
});
return builder;
You can use DI(constructor injection) by GenericHost.
IOptions<MyConfig> config;
ILogger<MyFirstBatch> logger;
public MyFirstBatch(IOptions<MyConfig> config, ILogger<MyFirstBatch> logger)
{
this.config = config;
this.logger = logger;
}
BatchContext is injected to property on method executing. It has four properties.
public string[] Arguments { get; private set; }
public DateTime Timestamp { get; private set; }
public CancellationToken CancellationToken { get; private set; }
public ILogger<BatchEngine> Logger { get; private set; }
MicroBatchFramework.WebHosting is support to expose web interface and swagger(with executable api document). It is useful for debugging.
NuGet: MicroBatchFramework.WebHosting
Install-Package MicroBatchFramework.WebHosting
public class Program
{
public static async Task Main(string[] args)
{
await new WebHostBuilder().RunBatchEngineWebHosting("http://localhost:12345");
}
}
in browser http://localhost:12345
, launch swagger ui.
dotnet publish to create executable file.
Here is the sample .config.yml
of CircleCI.
version: 2.1
executors:
dotnet:
docker:
- image: mcr.microsoft.com/dotnet/core/sdk:2.2
environment:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
NUGET_XMLDOC_MODE: skip
jobs:
publish-all:
executor: dotnet
steps:
- checkout
- run: dotnet publish -c Release --self-contained -r win-x64 -o ./bin/win-x64
- run: dotnet publish -c Release --self-contained -r linux-x64 -o ./bin/linux-x64
- run: dotnet publish -c Release --self-contained -r osx-x64 -o ./bin/osx-x64
- store_artifacts:
path: ./bin/
destination: ./bin/
workflows:
version: 2
publish:
jobs:
- publish-all
CLI tool can use .NET Core Global Tools. If you want to create it, check the Global Tools how to create.
If you hosting the batch to server, recommend to use container. Add Dockerfile like below.
FROM mcr.microsoft.com/dotnet/core/sdk:2.2 AS sdk
WORKDIR /workspace
COPY . .
RUN dotnet publish ./MicroBatchFrameworkSample.csproj -c Release -o /app
FROM mcr.microsoft.com/dotnet/core/runtime:2.2
COPY --from=sdk /app .
ENTRYPOINT ["dotnet", "MicroBatchFrameworkSample.dll"]
And docker build, send to any container registory. Here is the sample of deploy AWS ECR by CircleCI.
version: 2.1
orbs:
aws-ecr: circleci/aws-ecr@3.1.0
workflows:
build-push:
jobs:
# see: https://circleci.com/orbs/registry/orb/circleci/aws-ecr
- aws-ecr/build_and_push_image:
repo: "microbatchsample"
and set the AWS_ACCESS_KEY_ID
, AWS_SECRET_ACCESS_KEY
, AWS_ECR_ACCOUNT_URL, AWS_REGION
environment variables on CircleCI.
for example, run by AWS Batch, you can host easily and log can view on CloudWatch.
If you want to create complex workflow, you can use any worlkflow engine like luigi, Apache Airflow, etc.
If you host on AWS Batch, you can use CloudWatch Events to simple event scheduling trigger. If hosting to kubernetes, you can use Kubernetes CronJob.
This library is mainly developed by Yoshifumi Kawai(a.k.a. neuecc).
He is the CEO/CTO of Cysharp which is a subsidiary of Cygames.
He is awarding Microsoft MVP for Developer Technologies(C#) since 2011.
He is known as the creator of UniRx and MessagePack for C#.
This library is under the MIT License.