I'm trying to get an MVC6 app to be self-hosted for testing. I can do in-memory testing using TestServer, but for testing integration of multiple web apps, one of which includes a middleware that I have no control over that connects to the other app, I need at least one of the apps to be accessible over TCP.
I have tried using WebApp.Start, but it works with an IAppBuilder rather than IApplicationBuilder, so I can't get it to work with my Startup.
Is there any way to get an MVC6 app to be self-hosted in an xUnit test, via OWIN or any other way?
UPDATE:
FWIW, based on Pinpoint's answer and some additional research, I was able to come up with the following base class that works in xUnit, at least when the tests are in the same project as the MVC project:
public class WebTestBase : IDisposable
{
private IDisposable webHost;
public WebTestBase()
{
var env = CallContextServiceLocator.Locator.ServiceProvider.GetRequiredService<IApplicationEnvironment>();
var builder = new ConfigurationBuilder(env.ApplicationBasePath)
.AddIniFile("hosting.ini");
var config = builder.Build();
webHost = new WebHostBuilder(CallContextServiceLocator.Locator.ServiceProvider, config)
.UseEnvironment("Development")
.UseServer("Microsoft.AspNet.Server.WebListener")
.Build()
.Start();
}
public void Dispose()
{
webHost.Dispose();
}
}
Katana's WebApp static class has been replaced by WebHostBuilder, that offers a much more flexible approach: https://github.com/aspnet/Hosting/blob/dev/src/Microsoft.AspNet.Hosting/WebHostBuilder.cs.
You've probably already used this API without realizing it, as it's the component used by the hosting block when you register a new web command in your project.json (e.g Microsoft.AspNet.Hosting server=Microsoft.AspNet.Server.WebListener server.urls=http://localhost:54540) and run it using dnx (e.g dnx . web):
namespace Microsoft.AspNet.Hosting
{
public class Program
{
private const string HostingIniFile = "Microsoft.AspNet.Hosting.ini";
private const string ConfigFileKey = "config";
private readonly IServiceProvider _serviceProvider;
public Program(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void Main(string[] args)
{
// Allow the location of the ini file to be specified via a --config command line arg
var tempBuilder = new ConfigurationBuilder().AddCommandLine(args);
var tempConfig = tempBuilder.Build();
var configFilePath = tempConfig[ConfigFileKey] ?? HostingIniFile;
var appBasePath = _serviceProvider.GetRequiredService<IApplicationEnvironment>().ApplicationBasePath;
var builder = new ConfigurationBuilder(appBasePath);
builder.AddIniFile(configFilePath, optional: true);
builder.AddEnvironmentVariables();
builder.AddCommandLine(args);
var config = builder.Build();
var host = new WebHostBuilder(_serviceProvider, config).Build();
using (host.Start())
{
Console.WriteLine("Started");
var appShutdownService = host.ApplicationServices.GetRequiredService<IApplicationShutdown>();
Console.CancelKeyPress += (sender, eventArgs) =>
{
appShutdownService.RequestShutdown();
// Don't terminate the process immediately, wait for the Main thread to exit gracefully.
eventArgs.Cancel = true;
};
appShutdownService.ShutdownRequested.WaitHandle.WaitOne();
}
}
}
}
https://github.com/aspnet/Hosting/blob/dev/src/Microsoft.AspNet.Hosting/Program.cs
You can use Microsoft.AspNet.TestHost
See http://www.strathweb.com/2015/05/integration-testing-asp-net-5-asp-net-mvc-6-applications/ for details on use.
TestHost can work with your startup using a line like
TestServer dataServer = new TestServer(TestServer.CreateBuilder().UseStartup<WebData.Startup>());
where is the name of the application. The application has to be referenced in the test harness
Related
I'm using the slf4net.log4net nuget package to handle logging in a project. Because it must be possible for the loglevel to change at runtime, I made the configuration in code. The issue is that this code works fine in slf4net.log4net version 0.1.32.1 but when I upgrade it to version 1.0.0, the logfile is created, but the logs are not present on the logfile. I've created a dummy project to show this issue. I do not see how I can add a zip file here, so I'll just post the code here. It is a console app in net framework 4.7.2;
class Program
{
private static string GetLoggingPath()
{
var path = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData,
Environment.SpecialFolderOption.DoNotVerify), "LoggingTesting");
Directory.CreateDirectory(path);
return path;
}
static void Main(string[] args)
{
var layout = new PatternLayout
{
ConversionPattern = "%d{ABSOLUTE}: %message %newline"
};
layout.ActivateOptions();
var fileAppender = new RollingFileAppender();
fileAppender.RollingStyle = log4net.Appender.RollingFileAppender.RollingMode.Date;
fileAppender.Layout = layout;
var path = GetLoggingPath();
fileAppender.File = path + System.IO.Path.DirectorySeparatorChar + "LISlogging_.txt";
fileAppender.AppendToFile = true;
fileAppender.PreserveLogFileNameExtension = true;
fileAppender.StaticLogFileName = false;
fileAppender.DatePattern = "yyyy-MM-dd";
fileAppender.MaxSizeRollBackups = 10;
fileAppender.ActivateOptions();
ILoggerRepository repository = log4net.LogManager.GetRepository(Assembly.GetCallingAssembly());
BasicConfigurator.Configure(repository, fileAppender);
var root = (repository as Hierarchy)?.Root;
if (root == null) return;
root.Level = log4net.Core.Level.All;
// Create log4net ILoggerFactory and set the resolver
var factory = new slf4net.log4net.Log4netLoggerFactory();
var resolver = new SimpleFactoryResolver(factory);
slf4net.LoggerFactory.SetFactoryResolver(resolver);
// trigger logging
var log = slf4net.LoggerFactory.GetLogger(typeof(Program));
log.Info("log this line");
}
}
public class SimpleFactoryResolver : IFactoryResolver
{
private readonly slf4net.ILoggerFactory _factory;
public SimpleFactoryResolver(slf4net.ILoggerFactory factory)
{
_factory = factory;
}
public slf4net.ILoggerFactory GetFactory()
{
return _factory;
}
}
This dummy project was created in .net framework, but I need this in a .net core project. That is why I need to version 1.0.0 .
I've also post this issue on the github page of slf4net (because it looks like a bug) : https://github.com/ef-labs/slf4net/issues/6
My main question for here on stack overflow is if there is a workaround so this can work with slf4net.log4net version 1.0.0
I've found a workaround for this. Maybe not the cleanest solution but it works. If anyone knows a cleaner solution please add it here.
When looking at the slf4net.log4net code I found out that when it tries to configure log4net it uses xml files or config files, which is a nightmare if you want to set the loglevel at runtime. You can pass a customconfigurator as parameter of the Log4netLoggerFactory . This customconfigurator needs to implement IXmlConfigurator.
The CustomConfigurator I've made accepts an IAppender and a loglevel (log4net.Core.Level). In the implementation of the Configure(ICollection(ILoggerRepository repository) method. I've set the root log level and Configured with the BasicConfigurator.
The CustomConfigurator looks like this:
public class CustomConfigurator: IXmlConfigurator
{
private readonly IAppender _appender;
private readonly log4net.Core.Level _logLevel;
public CustomConfigurator(IAppender appender, log4net.Core.Level logLevel)
{
_appender = appender;
_logLevel = logLevel;
}
public ICollection Configure(ILoggerRepository repository)
{
var root = (repository as Hierarchy)?.Root;
if (root != null)
{
root.Level = _logLevel;
}
return BasicConfigurator.Configure(repository, _appender);
}
public ICollection Configure(ILoggerRepository repository, XmlElement element)
{
return XmlConfigurator.Configure(repository, element);
}
public ICollection Configure(ILoggerRepository repository, FileInfo configFile)
{
return XmlConfigurator.Configure(repository, configFile);
}
public ICollection ConfigureAndWatch(ILoggerRepository repository, FileInfo configFile)
{
return XmlConfigurator.ConfigureAndWatch(repository, configFile);
}
}
Now you can create an appender in code like shown in the question (until fileappender.ActivateOptions) Then when constructing the log4netLoggerFactory you pass an instance of CustomConfigurator which takes the fileAppender and a loglevel as parameter.
var factory = new slf4net.log4net.Log4netLoggerFactory(new CustomConfigurator(fileAppender, Level.All));
var resolver = new SimpleFactoryResolver(factory);
slf4net.LoggerFactory.SetFactoryResolver(resolver);
This should work.
I work on .NET Core 2.2 console application that uses Microsoft.Extensions.Logging and is configured to send logs to Azure Application Insights using Microsoft.ApplicationInsights.Extensibility by:
services.AddSingleton(x =>
new TelemetryClient(
new TelemetryConfiguration
{
InstrumentationKey = "xxxx"
}));
...
var loggerFactory = serviceProvider.GetService<ILoggerFactory>();
loggerFactory.AddApplicationInsights(serviceProvider, logLevel);
It works ok: I can read logs in Application Insights. But the application can be started simultanously in few instances (in different Docker containers). How can I distinguish traces from different instances? I can use source FileName, but I don't know how I should inject it.
I tried to use Scope:
var logger = loggerFactory.CreateLogger<Worker>();
logger.BeginScope(dto.FileName);
logger.LogInformation($"Start logging.");
It's interesting that my configuration is almost identical as in example: https://github.com/MicrosoftDocs/azure-docs/issues/12673
But in my case I can't see the property "FileName" in Application Insights.
For console project, if you want to use the custom ITelemetryInitializer, you should use this format: .TelemetryInitializers.Add(new CustomInitializer());
Official doc is here.
I test it at my side, and it works. The role name can be set.
Sample code is below:
static void Main(string[] args)
{
TelemetryConfiguration configuration = TelemetryConfiguration.CreateDefault();
configuration.InstrumentationKey = "xxxxx";
configuration.TelemetryInitializers.Add(new CustomInitializer());
var client = new TelemetryClient(configuration);
ServiceCollection services = new ServiceCollection();
services.AddSingleton(x => client);
var provider = services.BuildServiceProvider();
var loggerFactory = new LoggerFactory();
loggerFactory.AddApplicationInsights(provider, LogLevel.Information);
var logger = loggerFactory.CreateLogger<Program>();
logger.LogInformation("a test message 111...");
Console.WriteLine("Hello World!");
Console.ReadLine();
}
Check the role name in azure portal:
If you really have no way to distinguish them you can use a custom telemetry initializer like this:
public class CustomInitializer : ITelemetryInitializer
{
public void Initialize(ITelemetry telemetry)
{
telemetry.Context.Cloud.RoleName = Environment.MachineName;
}
}
and/or you can add a custom property:
public class CustomInitializer : ITelemetryInitializer
{
public void Initialize(ITelemetry telemetry)
{
if(telemetry is ISupportProperties)
{
((ISupportProperties)telemetry).Properties["MyIdentifier"] = Environment.MachineName;
}
}
}
In this example I used Environment.MachineName but you can of course use something else if needed. Like this work Id parameter of yours.
the wire it up using:
services.AddSingleton<ITelemetryInitializer, CustomInitializer>();
I'm using Ninject in a new Azure WebJobs project. One of my repositories requires a Db client to be passed. How do I pass this client?
My bindings class is:
public class NinjectBindings : Ninject.Modules.NinjectModule
{
public override void Load()
{
Bind<IMyRepository>().To<MyRepository>();
}
}
My Main function in the console app looks like this:
static void Main()
{
var kernel = new StandardKernel();
kernel.Load(Assembly.GetExecutingAssembly());
var config = new Configuration();
config.AddJsonFile("appsettings.json");
DbClient _dbClient = new DbClient(config);
IMyRepository myRepository = kernel.Get<IMyRepository>(); // This is where I get an error
}
My repository code is like this which is expecting the DbClient
public class MyRepository : IMyRepository
{
private DbClient _client;
public MyRepository(DbClient client)
{
_client = client;
}
}
You need to setup a binding for your DbClient.
I'd suggest being cautious around when components are released. I've not seen a good ninject example for web jobs yet so I've wired up manually. But that's just my thoughts...
I am trying to combine the registrations of the azure mobile service and the nservicebus registrations. When i try to inject the IBus into an controller it doesn't work.
public class AutofacConfig
{
public static void Register(HttpConfiguration http, ContainerBuilder container)
{
var busConfiguration = new BusConfiguration();
busConfiguration.UseContainer<AutofacBuilder>(c => c.ExistingLifetimeScope(container.Build()));
var bus = Bus.Create(busConfiguration);
bus.Start();
}
}
I need to call container.Build() in order to pass the current container to NServiceBus. but this results in an error in ServiceConfig.Initialize in my WebApiConfig class
public static class WebApiConfig
{
public static void Register()
{
// Use this class to set configuration options for your mobile service
ConfigOptions options = new ConfigOptions();
// Use this class to set WebAPI configuration options
HttpConfiguration config = ServiceConfig.Initialize(new ConfigBuilder(options, AutofacConfig.Register));
// To display errors in the browser during development, uncomment the following
// line. Comment it out again when you deploy your service for production use.
// config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;
Database.SetInitializer(new nos_devInitializer());
}
}
Ok, i see i can resolve the ILifetimeScope from the dependencyresolver. Call this method after ServiceConfig.Initialize and the two registrations are paired.
public class NServiceBusConfig
{
public static void Register(HttpConfiguration config)
{
var resolver = (AutofacWebApiDependencyResolver)config.DependencyResolver;
var scope = resolver.GetService<ILifetimeScope>();
var busConfiguration = new BusConfiguration();
busConfiguration.UseContainer<AutofacBuilder>(f => f.ExistingLifetimeScope(scope));
var bus = Bus.Create(busConfiguration);
bus.Start();
}
}
I need an instance of IDataProtectionProvider to generate email confirmation tokens using the Identity Framework UserManager in an Azure Web Jobs worker:
var confirmToken = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
This crashes because a null IUserTokenProvider<User, int> was passed to the UserManager<User, int> upon constuction.
In the MVC application an instance is created like this:
public class OWINStartup
{
public void Configuration(IAppBuilder app)
{
var dataProtectionProvider = app.GetDataProtectionProvider();
But of course, Azure Web Jobs doesn't have an OWINStartup hook. Any advice?
Taking a look at the Katana source code for the OWIN startup context you can see the default implementation of the DataProtectionProvider is a MachineKeyDataProtectionProvider. Unfortunately this class is not exposed to us, only the DpapiDataProtectionProvider which will not work when hosted in azure.
You can find the implementation of the MachineKeyDataProtectionProvider here. You will need to also implement your own MachineKeyDataProtector as seen here. These are not difficult implmentations and are essentially wrappers around MachineKey.Protect() and MachineKey.Unprotect().
The implementation for MachineKeyDataProtectionProvider and MachineKeyDataProtector from the Katana project source (apache 2.0 license):
internal class MachineKeyProtectionProvider : IDataProtectionProvider
{
public IDataProtector Create(params string[] purposes)
{
return new MachineKeyDataProtector(purposes);
}
}
internal class MachineKeyDataProtector : IDataProtector
{
private readonly string[] _purposes;
public MachineKeyDataProtector(string[] purposes)
{
_purposes = purposes;
}
public byte[] Protect(byte[] userData)
{
return MachineKey.Protect(userData, _purposes);
}
public byte[] Unprotect(byte[] protectedData)
{
return MachineKey.Unprotect(protectedData, _purposes);
}
}
Once you have that implemented it is easy to plug into the UserManager:
var usermanager = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>());
var machineKeyProtectionProvider = new MachineKeyProtectionProvider();
usermanager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(machineKeyProtectionProvider.Create("ASP.NET Identity"));
Hope that helps get you in the right direction.