Azure web job multiple connection strings - c#

Azure WebJob obtains connection string from web application (which runs the job) configuration parameter - AzureWebJobsStorage.
I need to monitor two queues in different storages using one WebJob.
Is it possible somehow to have multiple connection strings for a WebJob?

Related to this post it is possible :
servicebus webjob different connection string for output or trigger
In your case, you'd like to bind to differents storage accounts so your function can look like that:
public static void JobQueue1(
[QueueTrigger("queueName1"),
StorageAccount("storageAccount1ConnectionString")] string message)
{
}
public static void JobQueue2(
[QueueTrigger("queueName2"),
StorageAccount("storageAccount2ConnectionString")] string message)
{
}
You can also implement a custom INameResolver if you want to get the connectionstrings from the config :
public class ConfigNameResolver : INameResolver
{
public string Resolve(string name)
{
string resolvedName = ConfigurationManager.AppSettings[name];
if (string.IsNullOrWhiteSpace(resolvedName))
{
throw new InvalidOperationException("Cannot resolve " + name);
}
return resolvedName;
}
}
to use it:
var config = new JobHostConfiguration();
config.NameResolver = new ConfigNameResolver();
...
new JobHost(config).RunAndBlock();
And your new functions look like that:
public static void JobQueue1(
[QueueTrigger("queueName1"),
StorageAccount("%storageAccount2%")] string filename)
{
}
public static void JobQueue2(
[QueueTrigger("queueName2"),
StorageAccount("%storageAccount1%")] string filename)
{
}
storageAccount1 and storageAccount2 are the connection string key in the appSettings

Related

Custom log for each user with Serilog

I am trying to create a custom logger based on serilog. The application i am building is based on net6 blazor-server-side.
The goal is that every time a user logs into the application, I create a specific log file for him.
First I create a dependency injection in the program.cs
file Program.cs
builder.Services.AddScoped<ICustomLogger>( s => new CustomLogger());
In the Customlogger class, I initialize the loggerconfiguration in the constructor
file CustomLogger.cs
private ILogger<CustomLogger> _logger;
protected readonly LoggerConfiguration _loggerConfig;
public CustomLogger()
{
_loggerConfig = new LoggerConfiguration()
.Enrich.FromLogContext()
.MinimumLevel.Debug();
}
In the Login.razor , once the login is successful, I call the CreateLogger method, passing the username as a parameter (this is to create a specific folder)
file CustomLogger.cs
public void CreateLogger(string username)
{
var l = _loggerConfig.WriteTo.File($"./Logs/{username}/log_.txt", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 30).CreateLogger();
_logger = new SerilogLoggerFactory(l).CreateLogger<CustomLogger>(); // creates an instance of ILogger<CustomLogger>
}
Beyond that, I've created methods to write the various log levels
file CustomLogger.cs
public void LogInformation(string m)
{
_logger.LogInformation(m);
}
public void LogError(string m)
{
_logger.LogError(m);
}
public void LogWarning(string m)
{
_logger.LogWarning(m);
}
The Customlogger class is bound to the ICustomLogger interface
file ICustomLogger.cs
public interface ICustomLogger
{
void LogInformation(string m);
void LogError(string m);
void LogWarning(string m);
void CreateLogger(string username);
ILogger<CustomLogger> GetLogger();
}
For the moment I see that the system works, if I connect with a user, his folder and the file are created, and so on for each user.
My question is :
Could this approach cause problems?
Is it already possible to do this via Serilog?
Thanks for your time
N.
UPDATE
the system works well in the login page, for each user to create his own logger, but as soon as I move to the index page, the constructor of the CustomLogger class is called and the ILogger is null.
I thought AddScoped was only called once for "session"
Is it possible to call AddSingleton every time the user logs in, that way the specific dependency remains for as long as needed?
UPDATE 2
I changed the injection, now I use AddSingleton
Program.cs
builder.Services.AddSingleton<IOramsLoggerService>(s => new OramsLoggerService());
Inside the OramsLoggerService class, I created a list of loggers, which is filled at each login
OramsLoggerSerivce.cs
public class OramsLoggerService : IOramsLoggerService
{
private List<OramsLogger> loggers;
public OramsLoggerService()
{
loggers = new List<OramsLogger>();
}
public void CreateLogger(string? username)
{
if (string.IsNullOrEmpty(username))
{
throw new ArgumentNullException("username");
}
if (loggers.Where(x => x.Username == username).Count() > 0)
{
return;
}
// characters not allowed in the folder name
string originaUsername = username;
username = username.Replace("<", "-");
username = username.Replace(">", "-");
username = username.Replace(":", "-");
username = username.Replace("/", "-");
username = username.Replace("\\", "-");
username = username.Replace("|", "-");
username = username.Replace("?", "-");
username = username.Replace("*", "-");
username = username.Replace("\"", "-");
var loggerConfig = new LoggerConfiguration()
.Enrich.FromLogContext()
.MinimumLevel.Debug();
var l = loggerConfig.WriteTo.File($"./Logs/{username}/log_.txt", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 100).CreateLogger();
var logger = new SerilogLoggerFactory(l).CreateLogger<OramsLogger>(); // creates an instance of ILogger<OramsLogger>
loggers.Add(new OramsLogger(logger, originaUsername));
}
}
by doing so, I have all the loggers created in the other pages available.
The OramsLogger class contains the logger and the username.
I use the username to search the list, I think I'll change it to the id in the future.
OramsLogger.cs
public class OramsLogger
{
public ILogger<OramsLogger> logger;
public string Username { get; set; }
public OramsLogger(ILogger<OramsLogger> l, string username)
{
logger = l;
Username = username;
}
}
For the moment I have created 1000 dummy users when I login with my user and it seems to work.
Could this be a good approach?
For user-level custom logs, this method is appropriate.
There are no particular concerns.

Azure WebJobs access configuration from within a static function

I'm creating a webjob in .net core 3.1. In this project I have a function that is timer activated which should read the number of messages in a queue Q1 and if empty, put a message in Q2 as well as trigger a rest call to an API.
In order to check how many messages are in the API I need to access the AzureWebJobsStorage in my appsettings.json and then the url which is also in the settings.
Program.cs
class Program
{
static async Task Main()
{
var builder = new HostBuilder();
builder.ConfigureWebJobs(b =>
{
b.AddAzureStorageCoreServices();
b.AddAzureStorage();
b.AddTimers();
});
builder.ConfigureLogging((context, b) =>
{
b.AddConsole();
});
builder.ConfigureAppConfiguration((context, b) =>
{
b.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables();
});
builder.ConfigureServices((context, services) =>
{
var mySettings = new MySettings
{
AzureWebJobsStorage = context.Configuration.GetValue<string>("AzureWebJobsStorage"),
AzureWebJobsDashboard = context.Configuration.GetValue<string>("AzureWebJobsDashboard"),
url = context.Configuration.GetValue<string>("url"),
};
services.AddSingleton(mySettings);
});
var host = builder.Build();
using (host)
{
await host.RunAsync();
}
}
}
Fuctions.cs
public class Functions
{
public static void UpdateChannels([QueueTrigger("Q1")] string message, ILogger logger)
{
logger.LogInformation(message);
}
public static void WhatIsThereToUpdate([QueueTrigger("Q2")] string message, ILogger logger)
{
logger.LogInformation(message);
}
public static void CronJob([TimerTrigger("0 * * * * *")] TimerInfo timer, [Queue("Q2")] out string message, ILogger logger, MySettings mySettings)
{
message = null;
// Get the connection string from app settings
string connectionString = mySettings.AzureWebJobsStorage;
logger.LogInformation("Connection String: " + connectionString);
// Instantiate a QueueClient which will be used to create and manipulate the queue
QueueClient queueClient = new QueueClient(connectionString, "Q1");
if (queueClient.Exists())
{
QueueProperties properties = queueClient.GetProperties();
// Retrieve the cached approximate message count.
int cachedMessagesCount = properties.ApproximateMessagesCount;
// Display number of messages.
logger.LogInformation($"Number of messages in queue: {cachedMessagesCount}");
if (cachedMessagesCount == 0)
message = "Hello world!" + System.DateTime.Now.ToString(); //here I would call the REST API as well
}
logger.LogInformation("Cron job fired!");
}
}
appsettings.json
{
"AzureWebJobsStorage": "constr",
"AzureWebJobsDashboard": "constr",
"url": "url"
}
My Settings
public class MySettings
{
public string AzureWebJobsStorage { get; set; }
public string AzureWebJobsDashboard { get; set; }
public string url { get; set; }
}
However when I run this I get the following error:
Error indexing method 'Functions.CronJob'
Microsoft.Azure.WebJobs.Host.Indexers.FunctionIndexingException: Error indexing method 'Functions.CronJob'
---> System.InvalidOperationException: Cannot bind parameter 'mySettings' to type MySettings. Make sure the parameter Type is supported by the binding. If you're using binding extensions (e.g. Azure Storage, ServiceBus, Timers, etc.) make sure you've called the registration method for the extension(s) in your startup code (e.g. builder.AddAzureStorage(), builder.AddServiceBus(), builder.AddTimers(), etc.).
In addition to what is shown in the above codes I also tried using ConfigurationManager and Environment.GetEnvironmentVariable, both methods gave me null when I tried to read the values. For example ConfigurationManager.AppSettings.GetValues("AzureWebJobsStorage").
I also tried to register IConfiguration as a service services.AddSingleton(context.Configuration); and inject it in the parameters (instead of MySettings), but it also gave me the same binding error.
I'm really at a loss here, I've scoured the SO archives trying to find a solution and I think I tried everything I saw gave people positive results, but unfortunately I wasn't as lucky as the other posters.
Any guidance is much appreciated.
Edited to add my packages
In case it helps anyone, I'm using the following
Azure.Storage.Queues (12.4.0)
Microsoft.Azure.WebJobs.Extensions (3.0.6)
Microsoft.Azure.WebJobs.Extensions.Storage (4.0.2)
Microsoft.Extensions.Logging.Console (3.1.7)
When using DI, I suggest you use non-static method and constructor inject.
Here is the Functions.cs:
public class Functions
{
private readonly MySettings mySettings;
public Functions(MySettings _mySettings)
{
mySettings = _mySettings;
}
public void ProcessQueueMessage([TimerTrigger("0 */1 * * * *")] TimerInfo timer, [Queue("queue")] out string message, ILogger logger)
{
message = null;
string connectionString = mySettings.AzureWebJobsStorage;
logger.LogInformation("Connection String: " + connectionString);
}
}
No code change in other .cs file.
Here is the test result:

How to Enable/Disable Azure Function programmatically

Is there a way to programmatically enable/disable an Azure function?
I can enable/disable a function using the portal under the "Manage" section, which causes a request to be sent to https://<myfunctionapp>.scm.azurewebsites.net/api/functions/<myfunction>
The JSON payload looks a bit like:
{
"name":"SystemEventFunction",
"config":{
"disabled":true,
"bindings":[
// the bindings for this function
]
}
// lots of other properties (mostly URIs)
}
I'm creating a management tool outside of the portal that will allow users to enable and disable functions.
Hoping I can avoid creating the JSON payload by hand, so I'm wondering if there is something in an SDK (WebJobs??) that has this functionality.
Further to #James Z.'s answer, I've created the following class in C# that allows you to programmatically disable / enable an Azure function.
The functionsSiteRoot constructor argument is the Kudu root of your Functions application, eg https://your-functions-web-app.scm.azurewebsites.net/api/vfs/site/wwwroot/
The username and password can be obtained from "Get publish profile" in the App Service settings for your Functions.
public class FunctionsHelper : IFunctionsHelper
{
private readonly string _username;
private readonly string _password;
private readonly string _functionsSiteRoot;
private WebClient _webClient;
public FunctionsHelper(string username, string password, string functionsSiteRoot)
{
_username = username;
_password = password;
_functionsSiteRoot = functionsSiteRoot;
_webClient = new WebClient
{
Headers = { ["ContentType"] = "application/json" },
Credentials = new NetworkCredential(username, password),
BaseAddress = functionsSiteRoot
};
}
public void StopFunction(string functionName)
{
SetFunctionState(functionName, isDisabled: true);
}
public void StartFunction(string functionName)
{
SetFunctionState(functionName, isDisabled: false);
}
private void SetFunctionState(string functionName, bool isDisabled)
{
var functionJson =
JsonConvert.DeserializeObject<FunctionSettings>(_webClient.DownloadString(GetFunctionJsonUrl(functionName)));
functionJson.disabled = isDisabled;
_webClient.Headers["If-Match"] = "*";
_webClient.UploadString(GetFunctionJsonUrl(functionName), "PUT", JsonConvert.SerializeObject(functionJson));
}
private static string GetFunctionJsonUrl(string functionName)
{
return $"{functionName}/function.json";
}
}
internal class FunctionSettings
{
public bool disabled { get; set; }
public List<Binding> bindings { get; set; }
}
internal class Binding
{
public string name { get; set; }
public string type { get; set; }
public string direction { get; set; }
public string queueName { get; set; }
public string connection { get; set; }
public string accessRights { get; set; }
}
No, this is not possible currently. The disabled metadata property in function.json is what determines whether a function is enabled. The portal just updates that value when you enable/disable in the portal.
Not sure if it will meet your needs, but I'll point out that there is also a host.json functions array that can be used to control the set of functions that will be loaded (documented here). So for example, if you only wanted 2 of your 10 functions enabled, you could set this property to an array containing only those 2 function names (e.g. "functions": [ "QueueProcessor", "GitHubWebHook" ]), and only those will be loaded/enabled. However, this is slightly different than enable/disable in that you won't be able to invoke the excluded functions via the portal, whereas you can portal invoke disabled functions.
Further to #DavidGouge 's answer above, the code he posted does work, I just tested it and will be using it in my app. However it needs a couple of tweaks:
Remove the inheritance from IFunctionsHelper. I'm not sure what that interface is but it wasn't required.
Change the class definition for Binding as follows:
internal class Binding
{
public string name { get; set; }
public string type { get; set; }
public string direction { get; set; }
public string queueName { get; set; }
public string connection { get; set; }
public string accessRights { get; set; }
public string schedule { get; set; }
}
After that it would work.
P.S. I would have put this as a comment on the original answer, but I don't have enough reputation on Stack Overflow to post comments!
Using a combination of #Satya V's and #DavidGouge's solutions, I came up with this:
public class FunctionsHelper
{
private readonly ClientSecretCredential _tokenCredential;
private readonly HttpClient _httpClient;
public FunctionsHelper(string tenantId, string clientId, string clientSecret, string subscriptionId, string resourceGroup, string functionAppName)
{
var baseUrl =
$"https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Web/sites/{functionAppName}/";
var httpClient = new HttpClient
{
BaseAddress = new Uri(baseUrl)
};
_httpClient = httpClient;
_tokenCredential = new ClientSecretCredential(tenantId, clientId, clientSecret);
}
private async Task SetAuthHeader()
{
var accessToken = await GetAccessToken();
_httpClient.DefaultRequestHeaders.Authorization = AuthenticationHeaderValue.Parse($"Bearer {accessToken}");
}
private async Task<string> GetAccessToken()
{
return (await _tokenCredential.GetTokenAsync(
new TokenRequestContext(new[] {"https://management.azure.com/.default"}))).Token;
}
public async Task StopFunction(string functionName)
{
await SetFunctionState(functionName, isDisabled: true);
}
public async Task StartFunction(string functionName)
{
await SetFunctionState(functionName, isDisabled: false);
}
private async Task SetFunctionState(string functionName, bool isDisabled)
{
await SetAuthHeader();
var appSettings = await GetAppSettings();
appSettings.properties[$"AzureWebJobs.{functionName}.Disabled"] = isDisabled ? "1" : "0";
var payloadJson = JsonConvert.SerializeObject(new
{
kind = "<class 'str'>", appSettings.properties
});
var stringContent = new StringContent(payloadJson, Encoding.UTF8, "application/json");
await _httpClient.PutAsync("config/appsettings?api-version=2019-08-01", stringContent);
}
private async Task<AppSettings> GetAppSettings()
{
var res = await _httpClient.PostAsync("config/appsettings/list?api-version=2019-08-01", null);
var content = await res.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<AppSettings>(content);
}
}
internal class AppSettings
{
public Dictionary<string, string> properties { get; set; }
}
The problem with using the Kudu api to update the function.json file is that it will be overwritten on any subsequent deploy. This uses Azure's Rest Api to update the Configuration of the application. You will first need an Azure Service Principle to use the api though.
Using the Azure Cli, you can run az ad sp create-for-rbac to generate the Service Principle and get the client id and client secret. Because the UpdateConfiguration endpoint does not allow you to update a single value, and overwrites the entire Configuration object with the new values, you must first get all the current Configuration values, update the one you want, and then call the Update endpoint with the new Configuration keys and values.
I would imagine you can use Kudu REST API (specifically VFS) to update the disabled metadata property in function.json. Would that disable the function?
Here is the Kudu REST API. https://github.com/projectkudu/kudu/wiki/REST-API
The CLI command That is used to disable the Azure function through CLI - documented here
az functionapp config appsettings set --name <myFunctionApp> \
--resource-group <myResourceGroup> \
--settings AzureWebJobs.QueueTrigger.Disabled=true
I had captured fiddler while while running the above command.
Azure CLI works on the Python process The python process was issuing request to
https://management.azure.com to update appsetting.
got a reference to the same endpoint in the below REST Endpoint :
https://learn.microsoft.com/en-us/rest/api/appservice/webapps/updateapplicationsettings
Request URI :
PUT
https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Web/sites/{name}/config/appsettings?api-version=2019-08-01
Headers :
Authorization: Bearer <> ;
Content-Type: application/json; charset=utf-8
Request Body:
{"kind": "<class 'str'>", "properties":JSON}
We can hardcode the properties or get it dynamically. For disabling the function, will have to update the JSON node of Properties : Azure.WebJobs.QueueTrigger.Disabled = True
To get properties you could use the endpoint, you could refer Web Apps - List Application Settings
The Output looks up as below :
Hope this helps :)
What about this: https://learn.microsoft.com/en-us/azure/azure-functions/disable-function?tabs=portal#localsettingsjson
This looks like the easiest solution for local development.

C# WCF plugin design and implementation

I would like to get some advice. I am developing a system that will load up plugins at runtime and require them to be available through a WCF endpoint.
I will have a MVC 3 Web app that is only really used for configuration, and a class library (core) that will load up different plugins.
I would appreciate some guidance on how to go about this. I would like to load the plugin up and then be able to create a WCF endpoint that is registered with IIS 7 for access into that plugin.
Thanks in advance :)
Using a derivative of Darko's Dynamic IIS hosted WCF Service work, you can achieve something what you want. Let's start with an example service we might want to host, we'll call it an IMessageBroker, it's contract is simple:
[ServiceContract]
public interface IMessageBroker
{
[OperationContract]
string Send(string message);
}
We use this contract for both the Service, and the MEF Exports/Imports. We'll also define some additional metadata to go along with it:
public interface IMessageBrokerMetadata
{
public string Name { get; }
public string Channel { get; }
}
As it's a simple project, I'll cheat and use a simple static class for managing the MEF CompositionContainer used to compose parts:
public static class MEF
{
private static CompositionContainer container;
private static bool initialised;
public static void Initialise()
{
var catalog = new DirectoryCatalog("bin");
container = new CompositionContainer(catalog);
initialised = true;
}
public static CompositionContainer Container
{
get
{
if (!initialised) Initialise();
return container;
}
}
}
To be able to generate WCF Services dynamically, we need to create a ServiceHostFactory that can access our composition container to access our types, so you could do:
public class MEFServiceHostFactory : ServiceHostFactory
{
public override ServiceHostBase CreateServiceHost(string constructorString, System.Uri[] baseAddresses)
{
var serviceType = MEF.Container
.GetExports<IMessageBroker, IMessageBrokerMetadata>()
.Where(l => l.Metadata.Name == constructorString)
.Select(l => l.Value.GetType())
.Single();
var host = new ServiceHost(serviceType, baseAddresses);
foreach (var contract in serviceType.GetInterfaces())
{
var attr = contract.GetCustomAttributes(typeof(ServiceContractAttribute), true).FirstOrDefault();
if (attr != null)
host.AddServiceEndpoint(contract, new BasicHttpBinding(), "");
}
var metadata = host.Description.Behaviors
.OfType<ServiceMetadataBehavior>()
.FirstOrDefault();
if (metadata == null)
{
metadata = new ServiceMetadataBehavior();
metadata.HttpGetEnabled = true;
host.Description.Behaviors.Add(metadata);
}
else
{
metadata.HttpGetEnabled = true;
}
return host;
}
}
Essentially the constructorString argument is used to pass in the Metadata name we want for the specific service. Next up, we need to handle locating these services. What we now need is a VirtualPathProvider which we can use to dynamically create the instance, through a VirtualFile. The provider would look like:
public class ServiceVirtualPathProvider : VirtualPathProvider
{
private bool IsServiceCall(string virtualPath)
{
virtualPath = VirtualPathUtility.ToAppRelative(virtualPath);
return (virtualPath.ToLower().StartsWith("~/services/"));
}
public override VirtualFile GetFile(string virtualPath)
{
return IsServiceCall(virtualPath)
? new ServiceFile(virtualPath)
: Previous.GetFile(virtualPath);
}
public override bool FileExists(string virtualPath)
{
if (IsServiceCall(virtualPath))
return true;
return Previous.FileExists(virtualPath);
}
public override System.Web.Caching.CacheDependency GetCacheDependency(string virtualPath, System.Collections.IEnumerable virtualPathDependencies, DateTime utcStart)
{
return IsServiceCall(virtualPath)
? null
: Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}
}
What we are doing, is mapping any calls to /Services/ to our MEF derived endpoints. The service needs a virtual file, and this is where we tie it all together:
public class ServiceFile : VirtualFile
{
public ServiceFile(string virtualPath) : base(virtualPath)
{
}
public string GetName(string virtualPath)
{
string filename = virtualPath.Substring(virtualPath.LastIndexOf("/") + 1);
filename = filename.Substring(0, filename.LastIndexOf("."));
return filename;
}
public override Stream Open()
{
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
writer.Write("<%# ServiceHost Language=\"C#\" Debug=\"true\" Service=\"" + GetName(VirtualPath) +
"\" Factory=\"Core.MEFServiceHostFactory, Core\" %>");
writer.Flush();
stream.Position = 0;
return stream;
}
}
The virtual file will break out the Metadata name from the virtual path, where /Services/SampleMessageBroker.svc -> SampleMessageBroker. We then generate some markup which represents the markup of an .svc file with Service="SampleMessageBroker". This argument will be passed to the MEFServiceHostFactory where we can select out endpoints. So, given a sample endpoint:
[Export(typeof(IMessageBroker)),
ExportMetadata("Name", "SampleMessageBroker"),
ExportMetadata("Channel", "Greetings")]
public class SampleMessageBroker : IMessagerBroker
{
public string Send(string message)
{
return "Hello! " + message;
}
}
We can now access that dynamically at /Services/SampleMessageBroker.svc. What you might want to do, is provide a static service which allows you to interegate what endpoints are available, and feed that back to your consuming clients.
Oh, don't forget to wire up your virtual path provider:
HostingEnvironment.RegisterVirtualPathProvider(new ServiceVirtualPathProvider());

Programmatic configuration of Enterprise Library logging block

I've previously used log4net, but my current employer uses Enterprise Library application blocks. I had previously developed unit tests for my core logging classes as follows and was wondering if someone knew the equivalent for the OneTimeSetup code below for the logging app block (sorry for the long code post):
public abstract class DataGathererBase
{
public readonly log4net.ILog logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
public void CollectData()
{
this.LogDebug("Initialize started");
}
public static class Logger
{
private static LoggingSettings settings = LoggingSettings.GetLoggingSettings(new SystemConfigurationSource());
static Logger()
{
log4net.Config.XmlConfigurator.Configure();
}
public static void LogDebug(this DataGathererBase current, string message)
{
if (current.logger.IsDebugEnabled)
{
current.logger.Debug(string.Format("{0} logged: {1}", current.GetType().Name, message));
}
}
}
[TestFixture]
public class LoggerTests:DataGathererBase
{
private ListAppender appender;
private static ILog log;
[TestFixtureSetUp]
public void OneTimeSetup()
{
appender = new ListAppender();
appender.Layout = new log4net.Layout.SimpleLayout();
appender.Threshold = log4net.Core.Level.Fatal;
log4net.Config.BasicConfigurator.Configure(appender);
log = LogManager.GetLogger(typeof(ListAppender));
}
[Test]
public void TestLogging()
{
this.LogDebug("Debug");
Assert.AreEqual(0, ListAppender.logTable.Count());
}
}
Enterprise Library 5.0 introduced a fluent interface which can be used to programmatically configure the application blocks. You will probably find this to be a more comfortable option.
To give credit, this answer is based on a David Hayden article which is based on an Alois Kraus article, Programatic Configuraton - Enterprise Library (v2.0) Logging Block . Read those two articles for a good look at programmatic access to Enterprise Library logging.
I wasn't familiar with ListAppender so I created a CustomTraceListener that sticks the log messages in a List<string>:
public class ListAppender : Microsoft.Practices.EnterpriseLibrary.Logging.TraceListeners.CustomTraceListener
{
private List<string> list = new List<string>();
public override void Write(string message)
{
}
public override void WriteLine(string message)
{
list.Add(message);
}
public List<string> LogTable
{
get
{
return list;
}
}
}
Here is a modified LoggerTests class that programmatically accesses the EL logging classes to setup the tests (this does not use NUnit):
public class LoggerTests
{
private ListAppender appender;
private static LogWriter log;
public void OneTimeSetup()
{
appender = new ListAppender();
// Log all source levels
LogSource mainLogSource = new LogSource("MainLogSource", SourceLevels.All);
mainLogSource.Listeners.Add(appender);
// All messages with a category of "Error" should be distributed
// to all TraceListeners in mainLogSource.
IDictionary<string, LogSource> traceSources = new Dictionary<string, LogSource>();
traceSources.Add("Error", mainLogSource);
LogSource nonExistentLogSource = null;
log = new LogWriter(new ILogFilter[0], traceSources, nonExistentLogSource,
nonExistentLogSource, mainLogSource, "Error", false, false);
}
public void TestLogging()
{
LogEntry le = new LogEntry() { Message = "Test", Severity = TraceEventType.Information };
le.Categories.Add("Debug");
log.Write(le);
// we are not setup to log debug messages
System.Diagnostics.Debug.Assert(appender.LogTable.Count == 0);
le.Categories.Add("Error");
log.Write(le);
// we should have logged an error
System.Diagnostics.Debug.Assert(appender.LogTable.Count == 1);
}
}

Categories