Dependency Injection - directory inject one class in another class - c#

I have a MailRepository class with the following structire:
public class MailRepository : IMailRepository
{
public MailRepository()
{
}
public async Task SendMail(string subject, string content, string recipientAddress)
{
}
}
I also have a LocalizationReposiory class with the following code:
public class LocalizationRepository : ILocalizationRepository
{
private readonly IStringLocalizer<LocalizationRepository> _localizer = null;
public LocalizationRepository(IStringLocalizer<LocalizationRepository> localizer)
{
_localizer = localizer;
}
public string TranslateSetting(string settingName, params string[] additionalParams)
{
return _localizer.GetString(settingName, additionalParams);
}
}
This is how I call SendMail method in MailRepository from a class:
var subject = _localizationRepository.TranslateSetting("Subject");
var content = _localizationRepository.TranslateSetting("Body");
await _mailRepository.SendMail(subject, content, "xyz#yahoo.com");
This is how dependency injection in startup looks like:
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddSingleton<IMailRepository>(services => new MailRepository());
builder.Services.AddLocalization(opts => { opts.ResourcesPath = "Resources"; });
builder.Services.Configure<RequestLocalizationOptions>(opts =>
{
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("en-US")
};
opts.DefaultRequestCulture = new RequestCulture("en-US");
opts.SupportedCultures = supportedCultures;
opts.SupportedUICultures = supportedCultures;
});
builder.Services.AddSingleton<ILocalizationRepository, LocalizationRepository>();
}
}
Is there a way to inject the LocalizationRepository directly into MailRepository in order to avoid duplicating the following lines of code in multiple classes?
var subject = _localizationRepository.TranslateSetting("Subject");
var content = _localizationRepository.TranslateSetting("Body");
await _mailRepository.SendMail(subject, c,ontent "xyz#yahoo.com");

Inject ILocalizationRepository into MailRepository
public MailRepository(ILocalizationRepository localizationRepo)
{
//set private var
}
and then change how you register it in service provider
builder.Services.AddSingleton<IMailRepository, MailRepository>();

Related

How to inject dependencies into a MassTransitStateMachine activity?

I'm stuck and the docks for the lib are unhelpful. Given the below saga definition:
public class GetOrdersStateMachine : MassTransitStateMachine<GetOrdersState>
{
public State? FetchingOrdersAndItems { get; private set; }
public Event<GetOrders>? GetOrdersIntegrationEventReceived { get; private set; }
public GetOrdersStateMachine()
{
Initially(
When(GetOrdersIntegrationEventReceived)
.Activity(AddAccountIdToState)
.TransitionTo(FetchingOrdersAndItems));
}
private EventActivityBinder<GetOrdersState, GetOrders> AddAccountIdToState(
IStateMachineActivitySelector<GetOrdersState, GetOrders> sel) =>
sel.OfType<AddAccountIdToStateActivity>();
}
And the below activity definition:
public class AddAccountIdToStateActivity : Activity<GetOrdersState, GetOrders>
{
private readonly IPartnerService _partnerService;
public AddAccountIdToStateActivity(IPartnerService partnerService) => _partnerService = partnerService;
public void Probe(ProbeContext context) =>
context.CreateScope($"GetOrders{nameof(AddAccountIdToStateActivity)}");
public void Accept(StateMachineVisitor visitor) => visitor.Visit(this);
public async Task Execute(
BehaviorContext<GetOrdersState, GetOrders> context,
Behavior<GetOrdersState, GetOrders> next)
{
context.Instance.AccountId = await _partnerService.GetAccountId(context.Data.PartnerId);
await next.Execute(context);
}
public Task Faulted<TException>(
BehaviorExceptionContext<GetOrdersState, GetOrders, TException> context,
Behavior<GetOrdersState, GetOrders> next) where TException : Exception =>
next.Faulted(context);
}
And the below test definition:
var machine = new GetOrdersStateMachine();
var harness = new InMemoryTestHarness();
var sagaHarness = harness.StateMachineSaga<GetOrdersState, GetOrdersStateMachine>(machine);
var #event = new GetOrders("1", new[] {MarketplaceCode.De}, DateTime.UtcNow);
await harness.Start();
try
{
await harness.Bus.Publish(#event);
await harness.Bus.Publish<ListOrdersErrorResponseReceived>(new
{
#event.CorrelationId,
AmazonError = "test"
});
var errorMessages = sagaHarness.Consumed.Select<ListOrdersErrorResponseReceived>().ToList();
var sagaResult = harness.Published.Select<AmazonOrdersReceived>().ToList();
var state = sagaHarness.Sagas.Contains(#event.CorrelationId);
harness.Consumed.Select<GetOrders>().Any().Should().BeTrue();
sagaHarness.Consumed.Select<GetOrders>().Any().Should().BeTrue();
harness.Consumed.Select<ListOrdersErrorResponseReceived>().Any().Should().BeTrue();
errorMessages.Any().Should().BeTrue();
sagaResult.First().Context.Message.IsFaulted.Should().BeTrue();
errorMessages.First().Context.Message.CorrelationId.Should().Be(#event.CorrelationId);
errorMessages.First().Context.Message.AmazonError.Should().Be("test");
state.IsFaulted.Should().BeTrue();
}
finally
{
await harness.Stop();
}
As you can see, the AddAccountToStateActivity has a dependency on the IPartnerService. I can't figure a way to configure that dependency.There's nothing in the docs and neither can I find anything on the github. How do I do it?
Thanks to the help of one of the library's authors I ended up writing this code:
private static (InMemoryTestHarness harness, IStateMachineSagaTestHarness<GetOrdersState, GetOrdersStateMachine> sagaHarness) ConfigureAndGetHarnesses()
{
var provider = new ServiceCollection()
.AddMassTransitInMemoryTestHarness(cfg =>
{
cfg.AddSagaStateMachine<GetOrdersStateMachine, GetOrdersState>().InMemoryRepository();
cfg.AddSagaStateMachineTestHarness<GetOrdersStateMachine, GetOrdersState>();
})
.AddLogging()
.AddSingleton(Mock.Of<IPartnerService>())
.AddSingleton(Mock.Of<IStorage>())
.BuildServiceProvider(true);
var harness = provider.GetRequiredService<InMemoryTestHarness>();
var sagaHarness = provider
.GetRequiredService<IStateMachineSagaTestHarness<GetOrdersState, GetOrdersStateMachine>>();
return (harness, sagaHarness);
}
As you can see I'm registering my mocks with the ServiceProvider.

Call a class constructor with parameter inside the parent class

I have this class as you can see :
public class mybaseclass
{
public string token = "";
private readonly HttpContextAccessor iHTTP;
public mybaseclass([FromServices]HttpContextAccessor IHTTP)
{
//this.httpContext = httpContext;
iHTTP = IHTTP;
}
public mybaseclass()
{
}
protected Task<HttpRequestMessage> CreateHttpRequestMessageAsync(CancellationToken cancellationToken)
{
// var t = null;
try
{
// iHTTP.HttpContext.Request.Cookies[key]
var t = iHTTP.HttpContext.Request.Cookies["Authorization"];
if (t == null)
{
token = t;
}
}
catch(Exception aaa)
{
}
var msg = new HttpRequestMessage();
// SET THE BEARER AUTH TOKEN
msg.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
return Task.FromResult(msg);
}
}
And this class is called in this class :
public partial class Default1Client : mybaseclass, IDefault1Client
{
private string _baseUrl = "";
private System.Lazy<Newtonsoft.Json.JsonSerializerSettings> _settings;
public Default1Client(string baseUrl)
{
BaseUrl = baseUrl;
_settings = new System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(CreateSerializerSettings);
}
// other part of code
}
My problem is when I call Default1Client,the class calls the mybaseclass constructor that doesn't have any parameters ,but I need the constructor with httpcontextaccessorto be called
Modify your constructor to pass it through:
public Default1Client(string baseUrl, HttpContextAccessor contextAccessor) : base(contextAccessor)
{
//etc....
And populate it when you register:
services.AddScoped<IDefault1Client>(provider => { return new Default1Client
(
"localhost:44381",
provider.GetService(typeof(HttpContextAccessor)) as HttpContextAccesor
)});
In this case you want to specify the parameters, we can inject them using a function that will get the httpContextAccessor from your baseurl:
// you can use a function call to get the accessor
public Default1Client(string baseUrl) : mybaseclass(getHttpContextAccessor(baseUrl))
If you pass it the parameters that will correspond to httpcontextaccessor the constructor which takes parameters will be called.

How to let IOptionsMonitor<T> get the latest configuration value from a running .NET Core 2.2 app hosted on an Azure Windows Server VM?

So I have a .NET Core 2.2 app running on an Azure VM with Windows Server 2019 which has the following disk configuration:
The disk on the red box is where the App files are located. When the configuration file is updated either programatically or manually, IOptionsMonitor<T> is not picking up the changes.
As stated in this link:
As mentioned in the documentation, just enabling reloadOnChange and then injecting IOptionsSnapshot<T> instead of IOptions<T> will be enough. That requires you to have properly configured that type T though.
Which I did, as shown in this code:
private IConfiguration BuildConfig()
{
return new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("Config.json", false, reloadOnChange: true)
.Build();
}
public async Task MainAsync()
{
AppDomain.CurrentDomain.ProcessExit += ProcessExit;
...
IServiceCollection services = ConfigureServices();
// Configures the writable options from https://github.com/Nongzhsh/Awesome.Net.WritableOptions
services.ConfigureWritableOptions<ConfigurationSettings>(_config.GetSection("configurationSettings"), "ConfigDev.json");
// ConfigurationSettings is the POCO representing the config.json contents.
services.Configure<ConfigurationSettings>(_config.GetSection("configurationSettings"));
...
}
I haven't implemented the OnChange method since I'm assuming that the values should be automatically updated once the file's contents have changed. I have also tried setting the .NET Core's DOTNET_USE_POLLING_FILE_WATCHER to true but it did not work.
Here's is my code for reading and writing values to the configuration file:
public TimeService(
IServiceProvider provider,
IWritableOptions<ConfigurationSettings> writeOnlyOptions,
IOptionsMonitor<ConfigurationSettings> hotOptions)
{
_provider = provider;
_writeOnlyOptions = writeOnlyOptions;
_hotOptions = hotOptions;
}
private async Task EnsurePostedGameSchedules()
{
DateTime currentTime = DateTime.Now;
...
# region [WINDOWS ONLY] Lines for debugging.
// _hotOptions is the depency-injected IOptionsMonitor<T> object.
if (ConnectionState == ConnectionState.Connected)
{
await debugChannel.SendMessageAsync(
embed: RichInfoHelper.CreateEmbed(
"What's on the inside?",
$"Connection State: {ConnectionState}{Environment.NewLine}" +
$"Last Message ID: {_hotOptions.CurrentValue.LatestScheduleMessageID}{Environment.NewLine}" +
$"Last Message Timestamp (Local): {new ConfigurationSettings { LatestScheduleMessageID = Convert.ToUInt64(_hotOptions.CurrentValue.LatestScheduleMessageID) }.GetTimestampFromLastScheduleMessageID(true)}{Environment.NewLine}" +
$"Current Timestamp: {DateTime.Now}",
"").Build());
}
#endregion
if (new ConfigurationSettings { LatestScheduleMessageID = _hotOptions.CurrentValue.LatestScheduleMessageID }.GetTimestampFromLastScheduleMessageID(true).Date != currentTime.Date &&
currentTime.Hour >= 1)
{
...
try
{
...
if (gameScheds?.Count > 0)
{
if (gameSchedulesChannel != null)
{
// The line below updates the configuration file.
_writeOnlyOptions.Update(option =>
{
option.LatestScheduleMessageID = message?.Id ?? default;
});
}
}
}
catch (Exception e)
{
Console.WriteLine(e.Message + Environment.NewLine + e.StackTrace);
}
}
}
And here's the config POCO:
public class ConfigurationSettings
{
public string Token { get; set; }
public string PreviousVersion { get; set; }
public string CurrentVersion { get; set; }
public Dictionary<string, ulong> Guilds { get; set; }
public Dictionary<string, ulong> Channels { get; set; }
public ulong LatestScheduleMessageID { get; set; }
public string ConfigurationDirectory { get; set; }
public DateTime GetTimestampFromLastScheduleMessageID(bool toLocalTime = false) =>
toLocalTime ?
new DateTime(1970, 1, 1).AddMilliseconds((LatestScheduleMessageID >> 22) + 1420070400000).ToLocalTime() :
new DateTime(1970, 1, 1).AddMilliseconds((LatestScheduleMessageID >> 22) + 1420070400000);
}
Is there anything that I still need to do in order for IOptionsMonitor<T> to pick up the config changes in the config file?
EDIT: I forgot to tell how I configured the entire app. The program by the way is a long-running .NET Core console app (not a web app) so this is how the entire program is configured:
using ...
namespace MyProject
{
public class Program
{
static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult();
variables...
public async Task MainAsync()
{
AppDomain.CurrentDomain.ProcessExit += ProcessExit;
_client = new DiscordSocketClient();
_config = BuildConfig();
IServiceCollection services = ConfigureServices();
services.ConfigureWritableOptions<ConfigurationSettings>(_config.GetSection("configurationSettings"), "Config.json");
services.Configure<ConfigurationSettings>(_config.GetSection("configurationSettings"));
IServiceProvider serviceProvider = ConfigureServiceProvider(services);
serviceProvider.GetRequiredService<LogService>();
await serviceProvider.GetRequiredService<CommandHandlingService>().InitializeAsync(_config.GetSection("configurationSettings"));
serviceProvider.GetRequiredService<TimeService>().Initialize(_config.GetSection("configurationSettings"));
await _client.LoginAsync(TokenType.Bot, _config.GetSection("configurationSettings")["token"]);
await _client.StartAsync();
_client.Ready += async () =>
{
...
};
await Task.Delay(-1);
}
private void ProcessExit(object sender, EventArgs e)
{
try
{
...
}
catch (Exception ex)
{
...
}
}
private IServiceCollection ConfigureServices()
{
return new ServiceCollection()
// Base Services.
.AddSingleton(_client)
.AddSingleton<CommandService>()
// Logging.
.AddLogging()
.AddSingleton<LogService>()
// Extras. Is there anything wrong with this?
.AddSingleton(_config)
// Command Handlers.
.AddSingleton<CommandHandlingService>()
// Add additional services here.
.AddSingleton<TimeService>()
.AddSingleton<StartupService>()
.AddTransient<ConfigurationService>();
}
public IServiceProvider ConfigureServiceProvider(IServiceCollection services) => services.BuildServiceProvider();
private IConfiguration BuildConfig()
{
return new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("Config.json", false, true)
.Build();
}
}
}
It now worked without adding anything. I just let the app run using the compiled executable when I let my project target .NET Core 3.1. The app before was targeting .NET Core 2.2 and ran via PowerShell. I have no idea PowerShell has issues with IOptionsMonitor<T>.
According to my test, if we want to use IOptionsMonitor<T> to pick up the config changes in the config file, please refer to the following steps
My config.json
{
"configurationSettings": {
"Token": "...",
"PreviousVersion": "145.8.3",
"CurrentVersion": "145.23.4544",
"Guilds": {
"this setting": 4
},
"Channels": {
"announcements": 6
},
"LatestScheduleMessageID": 456,
"ConfigurationDirectory": "test"
}
}
My POCO
public class MyOptions
{
public string Token { get; set; }
public string PreviousVersion { get; set; }
public string CurrentVersion { get; set; }
public Dictionary<string, ulong> Guilds { get; set; }
public Dictionary<string, ulong> Channels { get; set; }
public ulong LatestScheduleMessageID { get; set; }
public string ConfigurationDirectory { get; set; }
public DateTime GetTimestampFromLastScheduleMessageID(bool toLocalTime = false) =>
toLocalTime ?
new DateTime(1970, 1, 1).AddMilliseconds((LatestScheduleMessageID >> 22) + 1420070400000).ToLocalTime() :
new DateTime(1970, 1, 1).AddMilliseconds((LatestScheduleMessageID >> 22) + 1420070400000);
}
Defile a class to save changes
public interface IWritableOptions<out T> : IOptions<T> where T : class, new()
{
void Update(Action<T> applyChanges);
}
public class WritableOptions<T> : IWritableOptions<T> where T : class, new()
{
private readonly IHostingEnvironment _environment;
private readonly IOptionsMonitor<T> _options;
private readonly string _section;
private readonly string _file;
public WritableOptions(
IHostingEnvironment environment,
IOptionsMonitor<T> options,
string section,
string file)
{
_environment = environment;
_options = options;
_section = section;
_file = file;
}
public T Value => _options.CurrentValue;
public T Get(string name) => _options.Get(name);
public void Update(Action<T> applyChanges)
{
var fileProvider = _environment.ContentRootFileProvider;
var fileInfo = fileProvider.GetFileInfo(_file);
var physicalPath = fileInfo.PhysicalPath;
var jObject = JsonConvert.DeserializeObject<JObject>(File.ReadAllText(physicalPath));
var sectionObject = jObject.TryGetValue(_section, out JToken section) ?
JsonConvert.DeserializeObject<T>(section.ToString()) : (Value ?? new T());
applyChanges(sectionObject);
jObject[_section] = JObject.Parse(JsonConvert.SerializeObject(sectionObject));
File.WriteAllText(physicalPath, JsonConvert.SerializeObject(jObject, Formatting.Indented));
}
}
Implemented an extension method for ServiceCollectionExtensions allowing you to easily configure a writable options
public static class ServiceCollectionExtensions
{
public static void ConfigureWritable<T>(
this IServiceCollection services,
IConfigurationSection section,
string file = "appsettings.json") where T : class, new()
{
services.Configure<T>(section);
services.AddTransient<IWritableOptions<T>>(provider =>
{
var environment = provider.GetService<IHostingEnvironment>();
var options = provider.GetService<IOptionsMonitor<T>>();
return new WritableOptions<T>(environment, options, section.Key, file);
});
}
}
Please add the following code in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
var configBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("Config.json", optional: false, reloadOnChange:true);
var config = configBuilder.Build();
services.ConfigureWritable<MyOptions>(config.GetSection("configurationSettings"));
...
}
Change the Json vaule
private readonly IWritableOptions<Locations> _writableLocations;
public OptionsController(IWritableOptions<Locations> writableLocations)
{
_writableLocations = writableLocations;
}
//Update LatestScheduleMessageID
public IActionResult Change(string value)
{
_writableLocations.Update(opt => {
opt.LatestScheduleMessageID = value;
});
return Ok("OK");
}
Read the JSON value
private readonly IOptionsMonitor<MyOptions> _options;
public HomeController(ILogger<HomeController> logger, IHostingEnvironment env, IOptionsMonitor<MyOptions> options)
{
_logger = logger;
_env = env;
_options = options;
}
public IActionResult Index()
{
var content= _env.ContentRootPath;
var web = _env.WebRootPath;
#ViewBag.Message = _options.CurrentValue.LatestScheduleMessageID;
return View();
}
Result
First
After change:

Property injection

I'm trying make a telegram bot with reminder. I'm using Telegram.Bot 14.10.0, Quartz 3.0.7, .net core 2.0. The first version should : get message "reminder" from telegram, create job (using Quartz) and send meaasage back in 5 seconds.
My console app with DI looks like:
Program.cs
static IBot _botClient;
public static void Main(string[] args)
{
// it doesn't matter
var servicesProvider = BuildDi(connecionString, section);
_botClient = servicesProvider.GetRequiredService<IBot>();
_botClient.Start(appModel.BotConfiguration.BotToken, httpProxy);
var reminderJob = servicesProvider.GetRequiredService<IReminderJob>();
reminderJob.Bot = _botClient;
Console.ReadLine();
_botClient.Stop();
// it doesn't matter
}
private static ServiceProvider BuildDi(string connectionString, IConfigurationSection section)
{
var rJob = new ReminderJob();
var sCollection = new ServiceCollection()
.AddSingleton<IBot, Bot>()
.AddSingleton<ReminderJob>(rJob)
.AddSingleton<ISchedulerBot>(s =>
{
var schedBor = new SchedulerBot();
schedBor.StartScheduler();
return schedBor;
});
return sCollection.BuildServiceProvider();
}
Bot.cs
public class Bot : IBot
{
static TelegramBotClient _botClient;
public void Start(string botToken, WebProxy httpProxy)
{
_botClient = new TelegramBotClient(botToken, httpProxy);
_botClient.OnReceiveError += BotOnReceiveError;
_botClient.OnMessage += Bot_OnMessage;
_botClient.StartReceiving();
}
private static async void Bot_OnMessage(object sender, MessageEventArgs e)
{
var me = wait _botClient.GetMeAsync();
if (e.Message.Text == "reminder")
{
var map= new Dictionary<string, object> { { ReminderJobConst.ChatId, e.Message.Chat.Id.ToString() }, { ReminderJobConst.HomeWordId, 1} };
var job = JobBuilder.Create<ReminderJob>().WithIdentity($"{prefix}{rnd.Next()}").UsingJobData(new JobDataMap(map)).Build();
var trigger = TriggerBuilder.Create().WithIdentity($"{prefix}{rnd.Next()}").StartAt(DateTime.Now.AddSeconds(5).ToUniversalTime())
.Build();
await bot.Scheduler.ScheduleJob(job, trigger);
}
}
}
Quartz.net not allow use constructor with DI. That's why I'm trying to create property with DI.
ReminderJob.cs
public class ReminderJob : IJob
{
static IBot _bot;
public IBot Bot { get; set; }
public async Task Execute(IJobExecutionContext context)
{
var parameters = context.JobDetail.JobDataMap;
var userId = parameters.GetLongValue(ReminderJobConst.ChatId);
var homeWorkId = parameters.GetLongValue(ReminderJobConst.HomeWordId);
await System.Console.Out.WriteLineAsync("HelloJob is executing.");
}
}
How can I pass _botClient to reminderJob in Program.cs?
If somebody looks for answer, I have one:
Program.cs (in Main)
var schedBor = servicesProvider.GetRequiredService<ISchedulerBot>();
var logger = servicesProvider.GetRequiredService<ILogger<DIJobFactory>>();
schedBor.StartScheduler();
schedBor.Scheduler.JobFactory = new DIJobFactory(logger, servicesProvider);
DIJobFactory.cs
public class DIJobFactory : IJobFactory
{
static ILogger<DIJobFactory> _logger;
static IServiceProvider _serviceProvider;
public DIJobFactory(ILogger<DIJobFactory> logger, IServiceProvider sp)
{
_logger = logger;
_serviceProvider = sp;
}
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{
IJobDetail jobDetail = bundle.JobDetail;
Type jobType = jobDetail.JobType;
try
{
_logger.LogDebug($"Producing instance of Job '{jobDetail.Key}', class={jobType.FullName}");
if (jobType == null)
{
throw new ArgumentNullException(nameof(jobType), "Cannot instantiate null");
}
return (IJob)_serviceProvider.GetRequiredService(jobType);
}
catch (Exception e)
{
SchedulerException se = new SchedulerException($"Problem instantiating class '{jobDetail.JobType.FullName}'", e);
throw se;
}
}
// get from https://github.com/quartznet/quartznet/blob/139aafa23728892b0a5ebf845ce28c3bfdb0bfe8/src/Quartz/Simpl/SimpleJobFactory.cs
public void ReturnJob(IJob job)
{
var disposable = job as IDisposable;
disposable?.Dispose();
}
}
ReminderJob.cs
public interface IReminderJob : IJob
{
}
public class ReminderJob : IReminderJob
{
ILogger<ReminderJob> _logger;
IBot _bot;
public ReminderJob(ILogger<ReminderJob> logger, IBot bot)
{
_logger = logger;
_bot = bot;
}
public async Task Execute(IJobExecutionContext context)
{
var parameters = context.JobDetail.JobDataMap;
var userId = parameters.GetLongValue(ReminderJobConst.ChatId);
var homeWorkId = parameters.GetLongValue(ReminderJobConst.HomeWordId);
await _bot.Send(userId.ToString(), "test");
}
}

log4net.LogicalThreadContext.Properties does not work correctly with OwinMiddleware

I am trying to use log4net.LogicalThreadContext to store some data in the OwinMiddleware, so i can log it later in the ApiController, but it doesn't seem to work. The data stored in log4net.LogicalThreadContext doesn't seems to make it available in the ApiController. Here is my code snippet:
Created the ApiMiddleWare in order to inject some log data to LogicalThreadContext.Properties["logdata"]:
public class ApiMiddleWare : OwinMiddleware
{
public ApiMiddleWare(OwinMiddleware next) : base(next)
{
}
public override async Task Invoke(IOwinContext context)
{
var loggers = new ConcurrentDictionary<string, object>();
if (!loggers.ContainsKey("CorellationId"))
{
var correlationId = new[] {Guid.NewGuid().ToString().Replace("-", "")};
loggers.TryAdd("CorellationId", correlationId[0]);
}
if (context.Request.Path.HasValue)
{
loggers.TryAdd("Route", context.Request.Uri.AbsoluteUri);
}
LogicalThreadContext.Properties["logdata"] = loggers;
await Next.Invoke(context);
}
}
Then ApiMiddleWare will be used in Startup.cs in ServiceHost as below:
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
app.Use<ApiMiddleWare>();
Log.Configure();
}
}
I created a custom RollingFileAppeanderEx to capture log data that was assigned in the middleware and log it:
public class RollingFileAppenderEx: RollingFileAppender
{
protected static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore,
DateFormatHandling = DateFormatHandling.IsoDateFormat
};
protected override void Append(LoggingEvent loggingEvent)
{
if (FilterEvent(loggingEvent))
{
var logdata = loggingEvent.GetLoggingEventData();
logdata.Message = GetLogData(loggingEvent);
loggingEvent = new LoggingEvent(loggingEvent.GetType(), loggingEvent.Repository, logdata, loggingEvent.Fix);
base.Append(loggingEvent);
}
}
protected string GetLogData(LoggingEvent logEvent)
{
IDictionary<string, object> logData = new Dictionary<string, object>();
var logD = logEvent.Properties["logdata"] as ConcurrentDictionary<string, object>;
if logD != null)
{
foreach (var log in logD)
{
logData.Add(log.Key, log.Value);
}
}
logData.Add("Message", logObject.Message);
var logString = JsonConvert.SerializeObject(logData, JsonSettings);
return logString;
}
}
From The ApiController, call Info function to log:
public class TestsController : ApiController
{
private static readonly ILogger Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
public string Get(int id)
{
Log.Info("Something");
return id;
}
}
Here is my problem: Only "Something" was written to the log. However, CorellationId and Route were not. Debugging through the code, I found that "logEvent.Properties["logdata"] as ConcurrentDictionary" returned nullable value in the RollingFileAppenderEx. So i have a theory: it seems that TestsController class is not in the same thread or not a child thread from ApiMiddleWare. Therefore, data stored in LogicalThreadContext does not propagate all the way.
If anyone can help to see if there is a way to do this, or maybe there is a bug in my code. I would appreciate it. Thanks.
Maybe you have to call loggingEvent.GetLoggingEventData()?
I had:
public abstract class AwsVerboseLogsAppender : AppenderSkeleton
{
// ...
protected override void Append(LoggingEvent loggingEvent)
{
// ...
}
// ...
}
// ...
var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "log4net.config");
var fileinfo = new FileInfo(path);
XmlConfigurator.Configure(fileinfo);
var log = LogManager.GetLogger(GetType());
LogicalThreadContext.Properties["Test"] = "MyValue";
log.Debug("test");
And loggingEvent.Properties was empty inside Append.
However if I called loggingEvent.GetLoggingEventData(), then "Test" and MyValue showed up inside loggingEvent.Properties. So maybe you have to call that method.

Categories

Resources