I'm currently writing an integration test (https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-5.0) for my ASP .Net Core 5 REST API.
The API is using Serilog for logging (with the static Serilog Logger). I am running tests with NUnit, Visual Studio 2019, Resharper.
I want all the messages, that are logged during the runtime of the API code, to be visible in the test console output.
For example, if this controller method is called:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Serilog;
namespace My.Crazy.Api.Controllers
{
public sealed class WheelsController : Controller
{
[HttpGet("getwheels")]
public async Task<IActionResult> Get()
{
Log.Error("An extremely urgent error");
return Ok();
}
}
}
I expect the "An extremely urgent error" message to be shown in the test console.
However, this is not happening.
Here is my TestServer setup:
[OneTimeSetUp]
public async Task Setup()
{
var hostBuilder = new HostBuilder()
.ConfigureWebHost(webHost =>
{
webHost.UseTestServer();
webHost.UseStartup<Startup>(); // Startup is the API project's Startup class
Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateLogger();
});
var host = await hostBuilder.StartAsync();
_client = host.GetTestClient();
}
[Test]
public async Task FirstTest()
{
var response = await _client.GetAsync("getwheels");
}
I have also tried logging with a custom Sink:
...
// in the test setup
Log.Logger = new LoggerConfiguration().WriteTo.Sink(new CustomSink()).CreateLogger();
...
public class CustomSink : ILogEventSink
{
public void Emit(LogEvent logEvent)
{
var message = logEvent.RenderMessage();
Console.WriteLine(message);
}
}
This does not work as well. However, I have confirmed that the Emit method is being invoked when API code logs any message.
Finally, I have tried using a File output:
Log.Logger = new LoggerConfiguration().WriteTo.File("C:\\temp\\test_output.txt").CreateLogger();
which worked as expected. However, I still want to log in the console.
Is this possible?
Using anything else for Serilog or NUnit is unfortunately not an option.
So I would try with a custom logger provider with logger:
LoggerProvider:
public class NUnitLoggerProvider : ILoggerProvider
{
public ILogger CreateLogger(string categoryName)
{
return new NUnitLogger();
}
public void Dispose()
{
}
}
Logger:
public class NUnitLogger : ILogger, IDisposable
{
public void Dispose()
{
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception,
Func<TState, Exception, string> formatter) {
var message = formatter(state, exception);
Debug.WriteLine(message);
}
public bool IsEnabled(LogLevel logLevel) => true;
public IDisposable BeginScope<TState>(TState state) => this;
}
Then in the test file:
var hostBuilder = new HostBuilder()
.ConfigureWebHost(webHost =>
{
webHost.UseTestServer()
.UseStartup<TestStartup>()
.ConfigureLogging((hostBuilderContext, logging) =>
{
logging.Services.AddSingleton<ILoggerProvider, NUnitLoggerProvider>();
});
});
And instead of Debug.WriteLine(message) you can use something else to log to.
I had the same problem. After days of digging, I found a workaround with the initialization of the test server. The key is in setting to true the PreserveExecutionContext which is by default false. Setting it to true brings the logs to the test output. False - no server logs are visible, only client ones.
var path = Assembly.GetAssembly(typeof(MyTestServer))?.Location;
var directoryName = Path.GetDirectoryName(path);
if (directoryName == null)
throw new InvalidOperationException("Cannot obtain startup directory name");
var hostBuilder = new WebHostBuilder()
.UseContentRoot(directoryName)
.ConfigureAppConfiguration(
configurationBuilder => configurationBuilder.AddJsonFile("appsettings.json", false))
.UseStartup<Startup>()
.ConfigureTestServices(services =>
{
//adding mock services here
});
server = new TestServer(hostBuilder)
{
//set this to true!!!
PreserveExecutionContext = true
};
Note: we're running these tests (and the system under test) on .NET7. I am not sure whether this makes any difference.
Related
I am trying to add NLog to a .NET 5 console app.
I understand I will want to not hard code some of these settings, and link a appsettings file soon, but I jsut want to get everything logging first, with the bare minimum.
So far I have:
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
var config = new LoggingConfiguration();
var fileTarget = new FileTarget("fileTarget")
{
FileName = #"c:\AppLogs\TestApp\mylog-${shortdate}.log",
Layout = "${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}"
};
if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development")
{
fileTarget = new FileTarget("fileTarget")
{
FileName = #"c:\AppLogs\TestApp_UAT\mylog-${shortdate}.log",
Layout = "${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}"
};
}
config.AddTarget(fileTarget);
// rules
config.AddRuleForOneLevel(NLog.LogLevel.Warn, fileTarget);
config.AddRuleForOneLevel(NLog.LogLevel.Error, fileTarget);
config.AddRuleForOneLevel(NLog.LogLevel.Fatal, fileTarget);
LogManager.Configuration = config;
var host = Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
{
services.AddTransient<TestService>();
}).ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
logging.AddNLog();
}).Build();
var svc = ActivatorUtilities.CreateInstance<TestService>(host.Services);
svc.Run();
}
I then try to log something in TestService:
public class TestService
{
public TestService(ILogger<TestService> logger)
{
Logger = logger;
}
public ILogger<TestService> Logger { get; }
public void Run()
{
Console.WriteLine("my first log");
Logger.LogInformation("my first log");
}
}
I don't get any errors, but I don't get any logs created (file or contents) created either. The console outputs, so TestServices runs correctly.
According to the docs, I was expecting to see a method called "BuildServiceProvider()" to chain after "ConfigureLogging()", but I only have 'Build()". Is this something to do with it, or have I missed something?
Here is my complete and encapsulated NLog (for Console/CommandLine/DotNetCore) applications.
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NLog;
using NLog.Extensions.Logging;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
namespace Me.Configuration.DependencyInjection
{
[ExcludeFromCodeCoverage]
public static class NlogSharedConfiguration
{
public const string NlogPerEnvironmentNameTemplate = "nlog.{0}.config";
public const string NlogDefaultFileName = "nlog.config";
public static IServiceCollection ConfigureSharedNlog(
this IServiceCollection services,
IConfiguration configuration,
IHostEnvironment hostEnvironmentProxy)
{
NLogProviderOptions nlpopts = new NLogProviderOptions
{
IgnoreEmptyEventId = true,
CaptureMessageTemplates = true,
CaptureMessageProperties = true,
ParseMessageTemplates = true,
IncludeScopes = true,
ShutdownOnDispose = true
};
/* Note, appsettings.json (or appsettings.ENVIRONMENT.json) control what gets sent to NLog. So the .json files must have the same (or more) detailed LogLevel set (compared to the Nlog setting)
* See https://stackoverflow.com/questions/47058036/nlog-not-logging-on-all-levels/47074246#47074246 */
services.AddLogging(
builder =>
{
builder.AddConsole().SetMinimumLevel(LogLevel.Trace);
builder.SetMinimumLevel(LogLevel.Trace);
builder.AddNLog(nlpopts);
});
string nlogPerEnvironmentName = string.Format(
NlogPerEnvironmentNameTemplate,
hostEnvironmentProxy.EnvironmentName);
string nlogConfigName = File.Exists(nlogPerEnvironmentName) ? nlogPerEnvironmentName : NlogDefaultFileName;
Console.WriteLine(string.Format("Nlog Configuration File. (FileName='{0}')", nlogConfigName));
if (!File.Exists(nlogConfigName))
{
throw new ArgumentOutOfRangeException(
string.Format("Nlog Configuration File NOT FOUND. (FileName='{0}')", nlogConfigName));
}
LogManager.LoadConfiguration(nlogConfigName);
NLogLoggerProvider nlogProv = new NLogLoggerProvider(nlpopts);
ILoggerProvider castLoggerProvider = nlogProv as ILoggerProvider;
services.AddSingleton<ILoggerProvider>(castLoggerProvider);
return services;
}
}
}
I have (in my rootfolder of my .exe.) the following files:
nlog.config
nlog.Development.config
NLog.xsd
nlog.Development.config is OPTIONAL, but this is how I have slightly different settings for a local developer vs everything else.
You can see my "if file exists" code above.
IHostEnvironment comes from here :
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-5.0#ihostenvironment
You are doing LogInformation but you have only enabled Warn + Error + Fatal using AddRuleForOneLevel. Maybe replace with AddRule(NLog.LogLevel.Info, NLog.LogLevel.Fatal, fileTarget) instead ?
You can also try specifying RemoveLoggerFactoryFilter (To avoid Microsoft ILogger-filtering problems):
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddNLog(new NLogProviderOptions { RemoveLoggerFactoryFilter = true );
})
See also: https://github.com/NLog/NLog.Extensions.Logging/blob/master/src/NLog.Extensions.Logging/Logging/NLogProviderOptions.cs
I'm trying to write some test with XUnit, specifically I'd like to have a test that ensures that when a certain exception is thrown it gets remapped into a meaningful error code.
I already set up the Global error handling middleware and it works correctly.
Here there is some example code of how my solution works:
My controller with a post endpoint that can return 200 or 404
//Controller
[HttpPost]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
public async Task<StatusCodeResult> Create([FromBody] Request request) {
//Process request
handler.Handle(request);
return Ok();
}
The Middleware for the Global error handling that remaps exceptions into Error codes
//StartUp Middleware
app.UseExceptionHandler(builder => {
builder.Run(handler: async context => {
IExceptionHandlerFeature error = context.Features.Get<IExceptionHandlerFeature>();
if (error != null) {
int statusCode = (int)GetStatusCodeForException(error.Error);
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(new ErrorDetails { StatusCode = statusCode, Message = error.Error.Message }.ToString());
}
});
});
And then my test in where I arrange some mocks, instantiate the controller and call the Create method
//UnitTest
[Fact]
public async Task Test()
{
//Arrange
var mockHandler = new Mock<IHandler>();
mockHandler.Setup(handler => handler.Handle(It.IsAny<Request>())).Throws(new CustomException(It.IsAny<string>()));
MyController myController = new MyController();
//Act
var statusCodeResult = await myController.Create(request);
//Assert
StatusCodeResult result = Assert.IsType<NotFoundResult>(statusCodeResult);
}
Here I want to ensure that the CustomException is remapped into a 404 status code. How do I do it? Any help is appreciated.
In your test the middleware is not available. You need to spin up a hosting environment to do that, the package Microsoft.AspNetCore.TestHost provides you with one that you can use for testing:
[Fact]
public async Task Test1()
{
using var host = new TestServer(Program.CreateHostBuilder(null));
var client = host.CreateClient();
var requestMessage = new HttpRequestMessage(HttpMethod.Get, "/api/controller");
var result = await client.SendAsync(requestMessage);
var status = result.StatusCode;
// TODO: assertions
}
Now when you call your API in a way an exception is thrown, the middleware should be executed and covered.
You can use the WebApplicationFactory class from the Microsoft.AspNetCore.Mvc.Testing nuget package. This bootstraps your application in-memory to allow for end to end functional tests. Rather than calling individual action methods you can make calls via HttpClient to ensure all middleware etc is called.
You use the Startup class that you have already defined in your main entry project and can add mock/fake objects to the IoC container as required. This allows you to verify/setup any dependencies.
You can then inject this as an IClassFixture. Once injected calling .CreateClient() on the instance will return an HttpClient, through which you can make requests.
Here is an example implementation:
// app factory instance
public class TestAppFactory : WebApplicationFactory<Startup>
{
// mock for setup/verify
public Mock<IHandler> MockHandler { get; } = new Mock<IHandler>();
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
services.AddSingleton(MockHandler);
});
}
}
public class MyTestClass : IClassFixture<TestAppFactory>
{
private readonly TestAppFactory _factory;
private readonly HttpClient _client;
private readonly Mock<IHandler> _mockHandler;
public MyTestClass(TestAppFactory factory)
{
_factory = factory;
_client = factory.CreateClient();
_mockHandler = factory.MockHandler;
}
[Fact]
public async Task Test()
{
// make calls via _client
}
}
With recent changes to the Azure Function App version 2 in Sept 2018, my function app code was refactored. However, it seems that:
LogTrace() messages no longer show in the console window (and I suspect Application Insights), and
categoryLevels in host.json does not seem to be respected.
The issue was duplicated in a sample application below in the call to LogWithILogger(). Two other points:
(1) I note that the default filter trace level seems to be hard-coded. Could another Filter be added to allow LogTrace() to work, or should LogTrace() no longer be used? If another Filter can be added, how does one inject the necessary objects into the Function App to permit that?
public static void Microsoft.Extensions.Logging.AddDefaultWebJobsFilter(this ILoggingBuilder builder)
{
builder.SetMinimumLevel(LogLevel.None);
builder.AddFilter((c,l) => Filter(c, l, LogLevel.Information));
}
(2) The Intellisense around LogLevel indicates:
LogLevel.Trace = 0
Logs that contain the most detailed messages. These messages may contain sensitive application data. These messages are disabled by default and should never be enabled in a production environment.
I would expect that LogTrace could be used for the console window when debugging - and would be controlled by the categoryLevel settings.
So, what should one be doing in terms of writing trace messages for a V2 Function app using ILogger? Thanks for the advice!
SAMPLE APPLICATION
Function1.cs
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Microsoft.Azure.WebJobs.Description;
using Microsoft.Azure.WebJobs.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
namespace FunctionAppTestLogging
{
[AttributeUsage(AttributeTargets.Parameter)]
[Binding]
public class InjectAttribute : Attribute
{
public InjectAttribute(Type type)
{
Type = type;
}
public Type Type { get; }
}
public class WebJobsExtensionStartup : IWebJobsStartup
{
public void Configure(IWebJobsBuilder webjobsBuilder)
{
webjobsBuilder.Services.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Trace).AddFilter("Function", LogLevel.Trace));
ServiceCollection serviceCollection = (ServiceCollection) webjobsBuilder.Services;
IServiceProvider serviceProvider = webjobsBuilder.Services.BuildServiceProvider();
// webjobsBuilder.Services.AddLogging();
// webjobsBuilder.Services.AddSingleton(new LoggerFactory());
// loggerFactory.AddApplicationInsights(serviceProvider, Extensions.Logging.LogLevel.Information);
}
}
public static class Function1
{
private static string _configuredLoggingLevel;
[FunctionName("Function1")]
public static async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]HttpRequest req, ILogger log, ExecutionContext context) // , [Inject(typeof(ILoggerFactory))] ILoggerFactory loggerFactory) // , [Inject(typeof(ILoggingBuilder))] ILoggingBuilder loggingBuilder)
{
LogWithILogger(log);
LogWithSeriLog();
SetupLocalLoggingConfiguration(context, log);
LogWithWrappedILogger(log);
return await RunStandardFunctionCode(req);
}
private static void SetupLocalLoggingConfiguration(ExecutionContext context, ILogger log)
{
var config = new ConfigurationBuilder()
.SetBasePath(context.FunctionAppDirectory)
.AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.Build();
// Access AppSettings when debugging locally.
string loggingLevel = config["LoggingLevel"]; // This needs to be set in the Azure Application Settings for it to work in the cloud.
_configuredLoggingLevel = loggingLevel;
}
private static void LogWithWrappedILogger(ILogger log)
{
LogWithWrappedILoggerHelper("This is Critical information from WrappedILogger", LogLevel.Critical, log);
LogWithWrappedILoggerHelper("This is Error information from WrappedILogger", LogLevel.Error, log);
LogWithWrappedILoggerHelper("This is Information information from WrappedILogger", LogLevel.Information, log);
LogWithWrappedILoggerHelper("This is Debug information from WrappedILogger", LogLevel.Debug, log);
LogWithWrappedILoggerHelper("This is TRACE information from WrappedILogger", LogLevel.Trace, log);
}
private static void LogWithWrappedILoggerHelper(string message, LogLevel messageLogLevel, ILogger log)
{
// This works as expected - Is the host.json logger section not being respected?
Enum.TryParse(_configuredLoggingLevel, out LogLevel logLevel);
if (messageLogLevel >= logLevel)
{
log.LogInformation(message);
}
}
private static void LogWithILogger(ILogger log)
{
var logger = log;
// Microsoft.Extensions.Logging.Logger _logger = logger; // Logger is protected - so not accessible.
log.LogCritical("This is critical information!!!");
log.LogDebug("This is debug information!!!");
log.LogError("This is error information!!!");
log.LogInformation("This is information!!!");
log.LogWarning("This is warning information!!!");
log.LogTrace("This is TRACE information!! from LogTrace");
log.Log(LogLevel.Trace, "This is TRACE information from Log", null);
}
private static void LogWithSeriLog()
{
// Code using the Serilog.Sinks.AzureTableStorage package per https://learn.microsoft.com/en-us/sandbox/functions-recipes/logging?tabs=csharp
/*
var serilog = new LoggerConfiguration()
.WriteTo.AzureTableStorage(connectionString, storageTableName: tableName, restrictedToMinimumLevel: LogEventLevel.Verbose)
.CreateLogger();
log.Debug("Here is a debug message {message}", "with some structured content");
log.Verbose("Here is a verbose log message");
log.Warning("Here is a warning log message");
log.Error("Here is an error log message");
*/
}
private static async Task<IActionResult> RunStandardFunctionCode(HttpRequest req)
{
string name = req.Query["name"];
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
name = name ?? data?.name;
return name != null
? (ActionResult)new OkObjectResult($"Hello, {name}")
: new BadRequestObjectResult("Please pass a name on the query string or in the request body");
}
}
}
host.json
{
"version": "2.0",
// The Azure Function App DOES appear to read from host.json even when debugging locally thru VS, since it complains if you do not use a valid level.
"logger": {
"categoryFilter": {
"defaultLevel": "Trace", // Trace, Information, Error
"categoryLevels": {
"Function": "Trace"
}
}
}
For v2 function, log setting in host.json has a different format.
{
"version": "2.0",
"logging": {
"fileLoggingMode": "debugOnly",
"logLevel": {
// For specific function
"Function.Function1": "Trace",
// For all functions
"Function":"Trace",
// Default settings, e.g. for host
"default": "Trace"
}
}
}
We occasionally have issues whereby someone adds some DI into a controller but forgets to add the relevant line into Startup.cs to set the scope of the object.
This does not prevent the application from starting, but rather, throws an exception when the relevent endpoint is hit.
Is there any way of programmatically checking all controllers are valid and preventing the application from starting otherwise?
Alternatively is there an easy way to write a catch-all automated test to check every controller can be instantiated using the specified DI in Startup.cs?
You can write it like this:
[TestFixture]
[Category(TestCategory.Integration)]
public class ControllersResolutionTest
{
[Test]
public void VerifyControllers()
{
var builder = new WebHostBuilder()
.UseStartup<IntegrationTestsStartup>();
var testServer = new TestServer(builder);
var controllersAssembly = typeof(UsersController).Assembly;
var controllers = controllersAssembly.ExportedTypes.Where(x => typeof(ControllerBase).IsAssignableFrom(x));
var activator = testServer.Host.Services.GetService<IControllerActivator>();
var serviceProvider = testServer.Host.Services.GetService<IServiceProvider>();
var errors = new Dictionary<Type, Exception>();
foreach (var controllerType in controllers)
{
try
{
var actionContext = new ActionContext(
new DefaultHttpContext
{
RequestServices = serviceProvider
},
new RouteData(),
new ControllerActionDescriptor
{
ControllerTypeInfo = controllerType.GetTypeInfo()
});
activator.Create(new ControllerContext(actionContext));
}
catch (Exception e)
{
errors.Add(controllerType, e);
}
}
if (errors.Any())
{
Assert.Fail(
string.Join(
Environment.NewLine,
errors.Select(x => $"Failed to resolve controller {x.Key.Name} due to {x.Value.ToString()}")));
}
}
}
This code actually goes through full process of setting up asp.net core application with database configuration and what not you have in you startup so you might want to derive from it and remove/mock some stuff. Also this code requires Microsoft.AspNetCore.TestHost nuget.
I changed original code that I posed as it was not working as expected.
Summarised from https://andrewlock.net/new-in-asp-net-core-3-service-provider-validation/, please see link for more details.
As of ASP.NET 3.0 there is now a way to validate controller dependencies on build:
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.AddControllersAsServices(); // This part adds Controllers to DI
Program.cs:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.UseDefaultServiceProvider((context, options) =>
{
options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
// Validate DI on build
options.ValidateOnBuild = true;
});
Notes:
Service provider validation is only enabled in the Development environment by default.
Will not work for run-time ServiceProvider look ups (service locator pattern) e.g. _service = provider.GetRequiredService<MyService>();
Will not work for [FromServices] parameters in methods (i.e. it only checks constructor dependencies)
Will not work for 'open generics' e.g. services.AddSingleton(typeof(MyServiceWithGeneric<>));
Will not work for services registered with factory functions e.g.
services.AddSingleton<MyService>(provider =>
{
var nestedService = provider.GetRequiredService<MyNestedService>();
return new MyService(nestedService);
});
Adapted #Rafal's answer to xUnit to avoid managing Exception iteration and skip dependency on TestHost:
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Routing;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace redacted.WebApi.Test {
using Core;
public class VerifyDependencies {
[Theory]
[MemberData(nameof(Controllers))]
public void VerifyController(Type controllerType) {
var services = new WebHostBuilder().UseStartup<Startup>().Build().Services;
ControllerUtilities.Create(
controllerType,
services.GetService<IControllerActivator>(),
services.GetService<IServiceProvider>()
);
}
public static IEnumerable<object[]> Controllers() {
return ControllerUtilities.GetControllers<ApiController>().Select(c => new object[] { c });
}
}
public class ControllerUtilities {
public static IEnumerable<Type> GetControllers<TProject>() {
return typeof(TProject)
.Assembly.ExportedTypes
.Where(x => typeof(Controller).IsAssignableFrom(x));
}
public static Controller Create(Type controllerType, IControllerActivator activator, IServiceProvider serviceProvider) {
return activator.Create(new ControllerContext(new ActionContext(
new DefaultHttpContext {
RequestServices = serviceProvider
},
new RouteData(),
new ControllerActionDescriptor {
ControllerTypeInfo = controllerType.GetTypeInfo()
})
)) as Controller;
}
public static TController Create<TController>(IControllerActivator activator, IServiceProvider serviceProvider) where TController : Controller {
return Create(typeof(TController), activator, serviceProvider) as TController;
}
}
}
The best method is to throw ArgumentNullExceptions. For example, in your controller's constructor:
_foo = foo ?? throw new ArgumentNullException(nameof(foo));
That will cause any action in the controller (any time the controller is constructed) to fail if the dependency is not satisified. Then, assuming you've got any sort of integration test suite around that controller, all your tests will instantly fail and you'll no exactly why: the constructor argument was not satisfied.
I'm sure Rafal's solution works, but I was unable to get it working. The IControllerActivator would not resolve, but I did end up figuring another way of doing this that does not require the Microsoft.AspNetCore.TestHost library. You will, however, need Microsoft.AspNetCore.Mvc.Core. Also, this is using MSTest.
[TestMethod]
public void ValidateDependencies()
{
// This is only necessary if you have reliance on the configuration.
// Make sure that your appsettings.json "Build Action" is "Content" and the "Copy to Output Directory" is "Copy if newer" or "Copy always"
var config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();
var svcCollection = new ServiceCollection();
// We've moved our dependencies to an extension method
svcCollection.RegisterServices(config);
var controllers = typeof(CustomerController).Assembly.ExportedTypes
.Where(x => !x.IsAbstract && typeof(ControllerBase).IsAssignableFrom(x)).ToList();
// By default, the controllers are not loaded so this is necessary
controllers.ForEach(c => svcCollection.AddTransient(c));
var serviceProvider = svcCollection.BuildServiceProvider();
var errors = new Dictionary<Type, Exception>();
foreach (Type controllerType in controllers)
{
try
{
serviceProvider.GetRequiredService(controllerType);
}
catch (Exception ex)
{
errors.Add(controllerType, ex);
}
}
if (errors.Any())
Assert.Fail(string.Join("\n",
errors.Select(x => $"Failed to resolve controller {x.Key.Name} due to {x.Value}")));
}
How can I use .NET Core's default dependency injection in Hangfire?
I am new to Hangfire and searching for an example which works with ASP.NET Core.
See full example on GitHub https://github.com/gonzigonz/HangfireCore-Example.
Live site at http://hangfirecore.azurewebsites.net/
Make sure you have the Core version of Hangfire:
dotnet add package Hangfire.AspNetCore
Configure your IoC by defining a JobActivator. Below is the config for use with the default asp.net core container service:
public class HangfireActivator : Hangfire.JobActivator
{
private readonly IServiceProvider _serviceProvider;
public HangfireActivator(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public override object ActivateJob(Type type)
{
return _serviceProvider.GetService(type);
}
}
Next register hangfire as a service in the Startup.ConfigureServices method:
services.AddHangfire(opt =>
opt.UseSqlServerStorage("Your Hangfire Connection string"));
Configure hangfire in the Startup.Configure method. In relationship to your question, the key is to configure hangfire to use the new HangfireActivator we just defined above. To do so you will have to provide hangfire with the IServiceProvider and this can be achieved by just adding it to the list of parameters for the Configure method. At runtime, DI will providing this service for you:
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory,
IServiceProvider serviceProvider)
{
...
// Configure hangfire to use the new JobActivator we defined.
GlobalConfiguration.Configuration
.UseActivator(new HangfireActivator(serviceProvider));
// The rest of the hangfire config as usual.
app.UseHangfireServer();
app.UseHangfireDashboard();
}
When you enqueue a job, use the registered type which usually is your interface. Don't use a concrete type unless you registered it that way. You must use the type registered with your IoC else Hangfire won't find it.
For Example say you've registered the following services:
services.AddScoped<DbManager>();
services.AddScoped<IMyService, MyService>();
Then you could enqueue DbManager with an instantiated version of the class:
BackgroundJob.Enqueue(() => dbManager.DoSomething());
However you could not do the same with MyService. Enqueuing with an instantiated version would fail because DI would fail as only the interface is registered. In this case you would enqueue like this:
BackgroundJob.Enqueue<IMyService>( ms => ms.DoSomething());
DoritoBandito's answer is incomplete or deprecated.
public class EmailSender {
public EmailSender(IDbContext dbContext, IEmailService emailService)
{
_dbContext = dbContext;
_emailService = emailService;
}
}
Register services:
services.AddTransient<IDbContext, TestDbContext>();
services.AddTransient<IEmailService, EmailService>();
Enqueue:
BackgroundJob.Enqueue<EmailSender>(x => x.Send(13, "Hello!"));
Source:
http://docs.hangfire.io/en/latest/background-methods/passing-dependencies.html
Note: if you want a full sample, see my blog post on this.
All of the answers in this thread are wrong/incomplete/outdated. Here's an example with ASP.NET Core 3.1 and Hangfire.AspnetCore 1.7.
Client:
//...
using Hangfire;
// ...
public class Startup
{
// ...
public void ConfigureServices(IServiceCollection services)
{
//...
services.AddHangfire(config =>
{
// configure hangfire per your requirements
});
}
}
public class SomeController : ControllerBase
{
private readonly IBackgroundJobClient _backgroundJobClient;
public SomeController(IBackgroundJobClient backgroundJobClient)
{
_backgroundJobClient = backgroundJobClient;
}
[HttpPost("some-route")]
public IActionResult Schedule([FromBody] SomeModel model)
{
_backgroundJobClient.Schedule<SomeClass>(s => s.Execute(model));
}
}
Server (same or different application):
{
//...
services.AddScoped<ISomeDependency, SomeDependency>();
services.AddHangfire(hangfireConfiguration =>
{
// configure hangfire with the same backing storage as your client
});
services.AddHangfireServer();
}
public interface ISomeDependency { }
public class SomeDependency : ISomeDependency { }
public class SomeClass
{
private readonly ISomeDependency _someDependency;
public SomeClass(ISomeDependency someDependency)
{
_someDependency = someDependency;
}
// the function scheduled in SomeController
public void Execute(SomeModel someModel)
{
}
}
As far as I am aware, you can use .net cores dependency injection the same as you would for any other service.
You can use a service which contains the jobs to be executed, which can be executed like so
var jobId = BackgroundJob.Enqueue(x => x.SomeTask(passParamIfYouWish));
Here is an example of the Job Service class
public class JobService : IJobService
{
private IClientService _clientService;
private INodeServices _nodeServices;
//Constructor
public JobService(IClientService clientService, INodeServices nodeServices)
{
_clientService = clientService;
_nodeServices = nodeServices;
}
//Some task to execute
public async Task SomeTask(Guid subject)
{
// Do some job here
Client client = _clientService.FindUserBySubject(subject);
}
}
And in your projects Startup.cs you can add a dependency as normal
services.AddTransient< IClientService, ClientService>();
Not sure this answers your question or not
Currently, Hangfire is deeply integrated with Asp.Net Core. Install Hangfire.AspNetCore to set up the dashboard and DI integration automatically. Then, you just need to define your dependencies using ASP.NET core as always.
If you are trying to quickly set up Hangfire with ASP.NET Core (tested in ASP.NET Core 2.2) you can also use Hangfire.MemoryStorage. All the configuration can be performed in Startup.cs:
using Hangfire;
using Hangfire.MemoryStorage;
public void ConfigureServices(IServiceCollection services)
{
services.AddHangfire(opt => opt.UseMemoryStorage());
JobStorage.Current = new MemoryStorage();
}
protected void StartHangFireJobs(IApplicationBuilder app, IServiceProvider serviceProvider)
{
app.UseHangfireServer();
app.UseHangfireDashboard();
//TODO: move cron expressions to appsettings.json
RecurringJob.AddOrUpdate<SomeJobService>(
x => x.DoWork(),
"* * * * *");
RecurringJob.AddOrUpdate<OtherJobService>(
x => x.DoWork(),
"0 */2 * * *");
}
public void Configure(IApplicationBuilder app, IServiceProvider serviceProvider)
{
StartHangFireJobs(app, serviceProvider)
}
Of course, everything is store in memory and it is lost once the application pool is recycled, but it is a quick way to see that everything works as expected with minimal configuration.
To switch to SQL Server database persistence, you should install Hangfire.SqlServer package and simply configure it instead of the memory storage:
services.AddHangfire(opt => opt.UseSqlServerStorage(Configuration.GetConnectionString("Default")));
I had to start HangFire in main function. This is how I solved it:
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();
using (var serviceScope = host.Services.CreateScope())
{
var services = serviceScope.ServiceProvider;
try
{
var liveDataHelper = services.GetRequiredService<ILiveDataHelper>();
var justInitHangfire = services.GetRequiredService<IBackgroundJobClient>();
//This was causing an exception (HangFire is not initialized)
RecurringJob.AddOrUpdate(() => liveDataHelper.RePopulateAllConfigDataAsync(), Cron.Daily());
// Use the context here
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "Can't start " + nameof(LiveDataHelper));
}
}
host.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
Actually there is an easy way for dependency injection based job registration.
You just need to use the following code in your Startup:
public class Startup {
public void Configure(IApplicationBuilder app)
{
var factory = app.ApplicationServices
.GetService<IServiceScopeFactory>();
GlobalConfiguration.Configuration.UseActivator(
new Hangfire.AspNetCore.AspNetCoreJobActivator(factory));
}
}
However i personally wanted a job self registration including on demand jobs (recurring jobs which are never executed, except by manual trigger on hangfire dashboard), which was a little more complex then just that. I was (for example) facing issues with the job service activation, which is why i decided to share most of my implementation code.
//I wanted an interface to declare my jobs, including the job Id.
public interface IBackgroundJob {
string Id { get; set; }
void Invoke();
}
//I wanted to retrieve the jobs by id. Heres my extension method for that:
public static IBackgroundJob GetJob(
this IServiceProvider provider,
string jobId) => provider
.GetServices<IBackgroundJob>()
.SingleOrDefault(j => j.Id == jobId);
//Now i needed an invoker for these jobs.
//The invoker is basically an example of a dependency injected hangfire job.
internal class JobInvoker {
public JobInvoker(IServiceScopeFactory factory) {
Factory = factory;
}
public IServiceScopeFactory Factory { get; }
public void Invoke(string jobId)
{
//hangfire jobs should always be executed within their own scope.
//The default AspNetCoreJobActivator should technically already do that.
//Lets just say i have trust issues.
using (var scope = Factory.CreateScope())
{
scope.ServiceProvider
.GetJob(jobId)?
.Invoke();
}
}
//Now i needed to tell hangfire to use these jobs.
//Reminder: The serviceProvider is in IApplicationBuilder.ApplicationServices
public static void RegisterJobs(IServiceProvider serviceProvider) {
var factory = serviceProvider.GetService();
GlobalConfiguration.Configuration.UseActivator(new Hangfire.AspNetCore.AspNetCoreJobActivator(factory));
var manager = serviceProvider.GetService<IRecurringJobManager>();
var config = serviceProvider.GetService<IConfiguration>();
var jobs = serviceProvider.GetServices<IBackgroundJob>();
foreach (var job in jobs) {
var jobConfig = config.GetJobConfig(job.Id);
var schedule = jobConfig?.Schedule; //this is a cron expression
if (String.IsNullOrWhiteSpace(schedule))
schedule = Cron.Never(); //this is an on demand job only!
manager.AddOrUpdate(
recurringJobId: job.Id,
job: GetJob(job.Id),
cronExpression: schedule);
}
//and last but not least...
//My Method for creating the hangfire job with injected job id
private static Job GetJob(string jobId)
{
var type = typeof(JobInvoker);
var method = type.GetMethod("Invoke");
return new Job(
type: type,
method: method,
args: jobId);
}
Using the above code i was able to create hangfire job services with full dependency injection support. Hope it helps someone.
Use the below code for Hangfire configuration
using eForms.Core;
using Hangfire;
using Hangfire.SqlServer;
using System;
using System.ComponentModel;
using System.Web.Hosting;
namespace eForms.AdminPanel.Jobs
{
public class JobManager : IJobManager, IRegisteredObject
{
public static readonly JobManager Instance = new JobManager();
//private static readonly TimeSpan ZeroTimespan = new TimeSpan(0, 0, 10);
private static readonly object _lockObject = new Object();
private bool _started;
private BackgroundJobServer _backgroundJobServer;
private JobManager()
{
}
public int Schedule(JobInfo whatToDo)
{
int result = 0;
if (!whatToDo.IsRecurring)
{
if (whatToDo.Delay == TimeSpan.Zero)
int.TryParse(BackgroundJob.Enqueue(() => Run(whatToDo.JobId, whatToDo.JobType.AssemblyQualifiedName)), out result);
else
int.TryParse(BackgroundJob.Schedule(() => Run(whatToDo.JobId, whatToDo.JobType.AssemblyQualifiedName), whatToDo.Delay), out result);
}
else
{
RecurringJob.AddOrUpdate(whatToDo.JobType.Name, () => RunRecurring(whatToDo.JobType.AssemblyQualifiedName), Cron.MinuteInterval(whatToDo.Delay.TotalMinutes.AsInt()));
}
return result;
}
[DisplayName("Id: {0}, Type: {1}")]
[HangFireYearlyExpirationTime]
public static void Run(int jobId, string jobType)
{
try
{
Type runnerType;
if (!jobType.ToType(out runnerType)) throw new Exception("Provided job has undefined type");
var runner = runnerType.CreateInstance<JobRunner>();
runner.Run(jobId);
}
catch (Exception ex)
{
throw new JobException($"Error while executing Job Id: {jobId}, Type: {jobType}", ex);
}
}
[DisplayName("{0}")]
[HangFireMinutelyExpirationTime]
public static void RunRecurring(string jobType)
{
try
{
Type runnerType;
if (!jobType.ToType(out runnerType)) throw new Exception("Provided job has undefined type");
var runner = runnerType.CreateInstance<JobRunner>();
runner.Run(0);
}
catch (Exception ex)
{
throw new JobException($"Error while executing Recurring Type: {jobType}", ex);
}
}
public void Start()
{
lock (_lockObject)
{
if (_started) return;
if (!AppConfigSettings.EnableHangFire) return;
_started = true;
HostingEnvironment.RegisterObject(this);
GlobalConfiguration.Configuration
.UseSqlServerStorage("SqlDbConnection", new SqlServerStorageOptions { PrepareSchemaIfNecessary = false })
//.UseFilter(new HangFireLogFailureAttribute())
.UseLog4NetLogProvider();
//Add infinity Expiration job filter
//GlobalJobFilters.Filters.Add(new HangFireProlongExpirationTimeAttribute());
//Hangfire comes with a retry policy that is automatically set to 10 retry and backs off over several mins
//We in the following remove this attribute and add our own custom one which adds significant backoff time
//custom logic to determine how much to back off and what to to in the case of fails
// The trick here is we can't just remove the filter as you'd expect using remove
// we first have to find it then save the Instance then remove it
try
{
object automaticRetryAttribute = null;
//Search hangfire automatic retry
foreach (var filter in GlobalJobFilters.Filters)
{
if (filter.Instance is Hangfire.AutomaticRetryAttribute)
{
// found it
automaticRetryAttribute = filter.Instance;
System.Diagnostics.Trace.TraceError("Found hangfire automatic retry");
}
}
//Remove default hangefire automaticRetryAttribute
if (automaticRetryAttribute != null)
GlobalJobFilters.Filters.Remove(automaticRetryAttribute);
//Add custom retry job filter
GlobalJobFilters.Filters.Add(new HangFireCustomAutoRetryJobFilterAttribute());
}
catch (Exception) { }
_backgroundJobServer = new BackgroundJobServer(new BackgroundJobServerOptions
{
HeartbeatInterval = new System.TimeSpan(0, 1, 0),
ServerCheckInterval = new System.TimeSpan(0, 1, 0),
SchedulePollingInterval = new System.TimeSpan(0, 1, 0)
});
}
}
public void Stop()
{
lock (_lockObject)
{
if (_backgroundJobServer != null)
{
_backgroundJobServer.Dispose();
}
HostingEnvironment.UnregisterObject(this);
}
}
void IRegisteredObject.Stop(bool immediate)
{
Stop();
}
}
}
Admin Job Manager
public class Global : System.Web.HttpApplication
{
void Application_Start(object sender, EventArgs e)
{
if (Core.AppConfigSettings.EnableHangFire)
{
JobManager.Instance.Start();
new SchedulePendingSmsNotifications().Schedule(new Core.JobInfo() { JobId = 0, JobType = typeof(SchedulePendingSmsNotifications), Delay = TimeSpan.FromMinutes(1), IsRecurring = true });
}
}
protected void Application_End(object sender, EventArgs e)
{
if (Core.AppConfigSettings.EnableHangFire)
{
JobManager.Instance.Stop();
}
}
}