Skip to content

Commit

Permalink
Ability to use injected IoC services within the IHost integrated model
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremydmiller committed Sep 23, 2024
1 parent 050f5e1 commit 26c5804
Show file tree
Hide file tree
Showing 10 changed files with 218 additions and 7 deletions.
3 changes: 2 additions & 1 deletion docs/.vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ function tableOfContents() {
{text: "Environment Checks", link: '/guide/host/environment'},
{text: "Writing Extension Commands", link: '/guide/host/extensions'},
{text: "The \"describe\" command", link: '/guide/host/describe'},
{text: "Stateful Resources", link: '/guide/host/resources'}
{text: "Stateful Resources", link: '/guide/host/resources'},
{text: "Using IoC Services", link: '/guide/host/ioc.md'}
]
},
{text: "Bootstrapping with CommandExecutor", link: '/guide/bootstrapping'},
Expand Down
1 change: 1 addition & 0 deletions docs/guide/host/integration_with_i_host.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,4 @@ There are just a couple things to note:
## Combining Serilog with Oakton

If you're having any issues with Serilog logging while using Oakton, please see this [StackOverflow issue](https://stackoverflow.com/questions/55422528/logging-with-serilog-net-core-not-outputting).

23 changes: 23 additions & 0 deletions docs/guide/host/ioc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Using IoC Services

Very frequently, folks have wanted to either use services from their IoC/DI container for their application, or to
have Oakton resolve the command objects from the application's DI container. New in Oakton 6.2 is that very ability.

## Injecting Services into Commands

If you are using [Oakton's IHost integration](/oakton/guide/host/integration_with_i_host), you can write commands that
use IoC services by simply decorating a publicly settable property on your Oakton command classes with the
new `[InjectService]` attribute.

First though, just to make sure you're clear about when and when this isn't applicable, this applies to Oakton used
from an `IHostBuilder` or `ApplicationBuilder` like so:

snippet: sample_using_ihost_activation

Then you can decorate your command types something like this:

snippet: sample_MyDbCommand

## Using IoC Command Creators


2 changes: 0 additions & 2 deletions src/Oakton/CommandFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ public CommandRun BuildRun(IEnumerable<string> args)
return buildRun(queue, CommandNameFor(DefaultCommand));
}


var firstArg = queue.Peek().ToLowerInvariant();

if (_helpCommands.Contains(firstArg))
Expand Down Expand Up @@ -209,7 +208,6 @@ private CommandRun buildRun(Queue<string> queue, string commandName)
var command = Build(commandName);

input ??= command.Usages.BuildInput(queue, _commandCreator);

var run = new CommandRun
{
Command = command,
Expand Down
17 changes: 15 additions & 2 deletions src/Oakton/CommandLineHostingExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using JasperFx.Core.Reflection;

namespace Oakton;

Expand Down Expand Up @@ -140,13 +141,25 @@ private static CommandExecutor buildExecutor(IHostBuilder source, Assembly? appl
{
factory.ApplyFactoryDefaults(applicationAssembly);
factory.ConfigureRun = cmd =>
factory.ConfigureRun = commandRun =>
{
if (cmd.Input is IHostBuilderInput i)
if (commandRun.Input is IHostBuilderInput i)
{
factory.ApplyExtensions(source);
i.HostBuilder = source;
}
else
{
var props = commandRun.Command.GetType().GetProperties().Where(x => x.HasAttribute<InjectServiceAttribute>())
.ToArray();
if (props.Any())
{
commandRun.Command = new HostWrapperCommand(commandRun.Command, source.Build, props);
}
}
};
});

Expand Down
38 changes: 38 additions & 0 deletions src/Oakton/HostWrapperCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Oakton.Help;

namespace Oakton;

internal class HostWrapperCommand : IOaktonCommand
{
private readonly IOaktonCommand _inner;
private readonly Func<IHost> _hostSource;
private readonly PropertyInfo[] _props;

public HostWrapperCommand(IOaktonCommand inner, Func<IHost> hostSource, PropertyInfo[] props)
{
_inner = inner;
_hostSource = hostSource;
_props = props;
}

public Type InputType => _inner.InputType;
public UsageGraph Usages => _inner.Usages;
public async Task<bool> Execute(object input)
{
using var host = _hostSource();
using var scope = host.Services.CreateScope();
foreach (var prop in _props)
{
var serviceType = prop.PropertyType;
var service = scope.ServiceProvider.GetRequiredService(serviceType);
prop.SetValue(_inner, service);
}

return await _inner.Execute(input);
}
}
12 changes: 12 additions & 0 deletions src/Oakton/InjectServiceAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;

namespace Oakton;

/// <summary>
/// Decorate Oakton commands that are being called by
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class InjectServiceAttribute : Attribute
{

}
2 changes: 0 additions & 2 deletions src/Tests/Tests.csproj.DotSettings

This file was deleted.

122 changes: 122 additions & 0 deletions src/Tests/using_injected_services.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Oakton;
using Oakton.Help;
using Shouldly;
using Xunit;

[assembly: OaktonCommandAssembly]

namespace Tests;

public class using_injected_services
{
[Fact]
public async Task can_use_injected_services()
{
var success = await Host.CreateDefaultBuilder()
.ConfigureServices(services =>
{
services.AddScoped<MyService>();
services.AddScoped<OtherService>();
})
.RunOaktonCommands(new string[] { "injected", "Bob Marley" });

success.ShouldBe(0);

MyService.WasCalled.ShouldBeTrue();
MyService.Name.ShouldBe("Bob Marley");
MyService.WasDisposed.ShouldBeTrue();

OtherService.WasCalled.ShouldBeTrue();
OtherService.Name.ShouldBe("Bob Marley");
OtherService.WasDisposed.ShouldBeTrue();

}
}

public class InjectedInput
{
public string Name { get; set; }
}

[Description("Injected command", Name = "injected")]
public class InjectedCommand : OaktonCommand<InjectedInput>
{
[InjectService]
public MyService One { get; set; }

[InjectService]
public OtherService Two { get; set; }

public override bool Execute(InjectedInput input)
{
One.DoStuff(input.Name);
Two.DoStuff(input.Name);

return true;
}
}

public class MyService : IDisposable
{
public static bool WasCalled;
public static string Name;

public static bool WasDisposed;

public void DoStuff(string name)
{
WasCalled = true;
Name = name;
}

public void Dispose()
{
WasDisposed = true;
}
}

public class OtherService : IDisposable
{
public static bool WasCalled;
public static string Name;

public static bool WasDisposed;

public void DoStuff(string name)
{
WasCalled = true;
Name = name;
}

public void Dispose()
{
WasDisposed = true;
}
}

public class MyDbContext{}

public class MyInput
{

}

#region sample_MyDbCommand

public class MyDbCommand : OaktonAsyncCommand<MyInput>
{
[InjectService]
public MyDbContext DbContext { get; set; }

public override Task<bool> Execute(MyInput input)
{
// do stuff with DbContext from up above
return Task.FromResult(true);
}
}

#endregion
5 changes: 5 additions & 0 deletions src/WorkerService/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

namespace WorkerService
{
#region sample_using_ihost_activation

public class Program
{
public static Task<int> Main(string[] args)
Expand All @@ -16,7 +18,10 @@ public static Task<int> Main(string[] args)
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
// This is a little old-fashioned, but still valid .NET core code:
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) => { services.AddHostedService<Worker>(); });
}

#endregion
}

0 comments on commit 26c5804

Please sign in to comment.