I have an AWS Serverless API application which is built with .NET Core (2.1). I have set this up with Serilog logging which uses the AWS Cloudwatch sink for it's logging destination. This project depends on a shared class library which is where the core logic resides.
I have noticed that not all my logging messages are making it into Cloudwatch. Messages in my API Controllers do get logged succesfully but messages from the shared class library are only logged when I'm running locally, not when using the live API.
Here is my logging setup in Startup.
var awsCredentials = new BasicAWSCredentials(config["AWS:AccessKey"], config["AWS:SecretKey"]);
if (env.IsProduction())
{
var options = new CloudWatchSinkOptions
{
LogGroupName = config["AWS:Cloudwatch:LogGroup"],
LogStreamNameProvider = new LogStreamNameProvider(),
TextFormatter = new Serilog.Formatting.Json.JsonFormatter()
};
var client = new AmazonCloudWatchLogsClient(awsCredentials, RegionEndpoint.EUWest2);
Log.Logger = new LoggerConfiguration()
.Enrich.WithExceptionDetails()
.WriteTo
.AmazonCloudWatch(options, client)
.CreateLogger();
}
services.AddSingleton(Log.Logger);
I've also tried adding services.AddLogging(builder => builder.AddSerilog()); but that hasn't helped.
I am using it like this:
public class TestService
{
private readonly ILogger logger;
public TestService(ILogger logger)
{
this.logger = logger;
}
public void TestMethod()
{
this.logger.Information("Test Message");
}
}
The default dependency injection framework has several limitations. Inside of a controller the dependencies will automatically load via placement in the constructor. However, in nested classes you have to pass the dependency itself, otherwise the framework will not automatically resolve. The framework works under the presumption of a composition root. In essence:
// Auto Resolved:
public class SampleController : Controller
{
private readonly ILogger logger;
public SampleController(ILogger logger) => this.logger = logger;
public IActionResult Index()
{
logger.Information("...");
..
}
}
// Will not auto resolve
public class SampleService
{
private readonly ILogger logger;
public SampleService(ILogger logger) => this.logger = logger;
public void SampleAction()
{
logger.Information("...");
..
}
}
For you to correctly implement the logger within the SampleService you would need to do something along these lines:
// Logger as property
Logger = serviceProvider.GetService<ILogger>();
var sampleService = new SampleService(Logger);
// Called directly passed
var sampleService = new SampleService(serviceProvider.GetService<ILogger>());
That has been my experience, otherwise it will fail to use the defined implementation you passed. There could be information I do not have that may also be affecting your code.
Related
In our .Net Core 3.1 project we log using Serilog
In the set-up of my test I create a ServiceCollection so I can new up my classes I use in the tests using a service collection \ - provider.
I have a repository that uses both a DbContext and a Serilog logger.
using Serilog;
using Microsoft.EntityFrameworkCore;
namespace MyApp.Persistency
{
public class MyRepository : IMyRepository
{
private readonly DataContext _dataContext;
private readonly ILogger _logger;
public OpvragenOnbekendeBurgerRegistratieRepository(DataContext dataContext, ILogger logger)
{
_dataContext = dataContext;
_logger = logger;
}
...
}
In my test class:
[TestInitialize]
public void Initialize()
{
var diCollection =
new ServiceCollection()
.AddSingleton(new SqlConnectionOption(_connectionstring))
.AddDbContext<DataContext>(
options =>
{
options.UseSqlServer(_connectionstring);
}
)
...
xxx Add some kind of Serilog registration xxx
...
.AddTransient<IMyRepositoy,MyRepository>();
_di = diCollection.BuildServiceProvider;
}
[TestMethod]
public void MyRepositoryTest()
{
// arrange
var myRepository = _di.GetRequiredService<IMyRepository>();
...
}
I have seen a zillion different code samples, but none seem to work only a container, instead of a full
blown host. A dummy that logs nothing would be acceptable, but logging to the testconsole would of course be epic.
Add Serilog to a ServiceCollection instance:
new ServiceCollection()
//...
.AddLogging(builder => builder.AddSerilog(dispose: true))
//...
Serilog configuration:
Log.Logger = new LoggerConfiguration()
//...
.CreateLogger();
Documentation
I'm trying to attach Serilog to Dotnet core logging framework in a console application. Everything works great in terms of configuration. However, I cannot somehow get the Dependency Injection to work.
For this logging approach that I'm following, each class that needs logging will have to work with ILogger field. The problem that I have is how to register ILogger service as an open-generic type(So I can pass any ILogger<>). When I run my current code I get this exception:
Open generic service type
'Microsoft.Extensions.Logging.ILogger`1[TCategoryName]' requires
registering an open generic implementation type. (Parameter
'descriptors')
I have looked at the thread over here but it seems like my problem is a bit different.
This is my code:
The "Test" is a class that needs logging functionality:
public class Test
{
private readonly ILogger _logger;
public Test(ILogger<Test> logger)
{
_logger = logger;
}
public void DoSomething()
{
//code here
_logger.LogInformation("This is the logged error");
}
}
In the Main I have something like:
var log = serviceProvider.GetService<ILogger<Test>>();
new Test(log).DoSomething();
And this is how I configure my ILogger service:
public static IServiceCollection ConfigureSerilog(this IServiceCollection services)
{
var logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Warning)
.WriteTo.Console(restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Information)
.WriteTo.File(#"Logs\orgManager.log", rollingInterval: RollingInterval.Day)
.WriteTo.Sink(new TelegramSink(), LogEventLevel.Error)
.CreateLogger();
ApplicationLogging.LoggerFactory.AddSerilog(logger);
services.AddSingleton(typeof(ILogger<>), s => ApplicationLogging.CreateLogger(typeof(ILogger<>)));
return services;
}
This is the ApplocationLogging class(accessible throughout the application):
public static class ApplicationLogging
{
public static ILoggerFactory LoggerFactory { get; } = new LoggerFactory();
public static ILogger CreateLogger(Type t)
{
return LoggerFactory.CreateLogger(t);
}
}
I have a dotnet core web api application and I am creating a custom class outside of the standard folders.
/Controllers/
/Components/CustomClass.cs
Startup.cs
So my CustomClass looks like:
public class CustomClass
{
private readonly ILogger<CustomClass> _logger;
public CustomClass()
{
}
}
I see that there is a ILogger interface here: https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.ilogger?view=dotnet-plat-ext-3.0
In my dotnet core Web API, do I have to use dependancy injection somehow to get the logger that my web API is using?
I want to use my CustomClass inside of a controller, how do I use that Logger in this scenerio?
public class CustomClass
{
private readonly ILogger<CustomClass> _logger;
public CustomClass(ILogger logger)
{
_logger = logger;
}
}
Register everything in Startup.
Console apps don't use the Startup file with configure services like web apps do and I'm struggling to understand the crucial concept of Dependency Injection.
(Please note the below example does not compile)
Here is a basic example of how I think it should work (please do point out anything unconventional or wrong):
static void Main(string[] args)
{
var builder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddUserSecrets<Settings>()
.Build();
var services = new ServiceCollection()
.AddLogging(b => b
.AddConsole())
.AddDbContext<UnderstandingDIContext>(options =>
options.UseSqlite(builder.GetConnectionString("DefaultConnection")))
.BuildServiceProvider();
var logger = services.GetService<ILoggerFactory>()
.CreateLogger<Program>();
logger.LogInformation("Starting Application");
var worker = new Worker();
logger.LogInformation("Closing Application");
}
But how do I use these services inside my 'Worker' class?:
public Worker(ILogger logger, IConfiguration configuration)
{
logger.LogInformation("Inside Worker Class");
var settings = new Settings()
{
Secret1 = configuration["Settings:Secret1"],
Secret2 = configuration["Settings:Secret2"]
};
logger.LogInformation($"Secret 1 is '{settings.Secret1}'");
logger.LogInformation($"Secret 2 is '{settings.Secret2}'");
using (var context = new UnderstandingDIContext())
{
context.Add(new UnderstandingDIModel()
{
Message = "Adding a message to the database."
});
}
}
UnderstandingDIContext
public class UnderstandingDIContext : DbContext
{
public UnderstandingDIContext(DbContextOptions<UnderstandingDIContext> options)
: base(options)
{ }
public DbSet<UnderstandingDIModel> UnderstandingDITable { get; set; }
}
The problems with this code are as follows:
Worker() is expecting to be passed ILogger and IConfiguration parameters but I thought Dependency Injection should cover that?
I cannot run 'dotnet ef migrations add Initial' because I'm not correctly passing in the connection string (error: 'Unable to create an object of type 'UnderstandingDIContext'.')
'using (var context = new UnderstandingDIContext())' won't compile because I'm misunderstanding the DbContext bit.
I've searched around A LOT and there's lots of examples for web apps but very little for Console apps. Am I just completely misunderstanding the entire concept of Dependency Injection?
When using constructor injection, dependencies will only be resolved when the object you are creating is actually created through dependency injection itself. So the key to make dependency injection work within your Worker is to actually resolve Worker through the dependency injection container as well.
This is actually pretty simple:
var services = new ServiceCollection()
.AddLogging(b => b.AddConsole())
.AddDbContext<UnderstandingDIContext>(options =>
options.UseSqlite(builder.GetConnectionString("DefaultConnection")));
// register `Worker` in the service collection
services.AddTransient<Worker>();
// build the service provider
var serviceProvider = services.BuildServiceProvider();
// resolve a `Worker` from the service provider
var worker = serviceProvider.GetService<Worker>();
var logger = serviceProvider.GetService<ILogger<Program>>();
logger.LogInformation("Starting Application");
worker.Run();
logger.LogInformation("Closing Application");
In addition, since you are using a database context which gets registered as a scoped dependency by default, I would recommend you to create a service scope as well—or alternatively change the lifetime of the database context when you register it.
var serviceProvider = services.BuildServiceProvider();
using (var scope = serviceProvider.CreateScope())
{
var worker = serviceProvider.GetService<Worker>();
worker.Run();
}
Note that I also made an explicit method Run on your worker, so that you don’t have the logic within the constructor.
public class Worker
{
private readonly ILogger<Worker> _logger = logger;
private readonly IConfiguration _configuration = configuration;
private readonly UnderstandingDIContext _dbContext = dbContext;
public Worker(ILogger<Worker> logger, IConfiguration configuration, UnderstandingDIContext dbContext)
{
_logger = logger;
_configuration = configuration;
_dbContext = dbContext;
}
public void Run()
{
_logger.LogInformation("Inside Worker Class");
var settings = new Settings()
{
Secret1 = configuration["Settings:Secret1"],
Secret2 = configuration["Settings:Secret2"]
};
_logger.LogInformation($"Secret 1 is '{settings.Secret1}'");
_logger.LogInformation($"Secret 2 is '{settings.Secret2}'");
_dbContext.Add(new UnderstandingDIModel()
{
Message = "Adding a message to the database."
});
_dbContext.SaveChanges();
}
}
Take a look at this API.
https://github.com/Akeraiotitasoft/ConsoleDriving
Nuget: Akeraiotitasoft.ConsoleDriving
Setup
Windows 10
Visual Studio Professional 2017 v15.9.9
ASP.NET Core 2.2
EF Core 2.2
Dapper
xUnit 2.4.1
Description
I'm using the WebApplicationFactory from the Microsoft.AspNetCore.Mvc.Testing package to setup my integration tests.
I've been following the official documentation to customise the web host configuration.
The SUT uses Dapper to query from the database, so I'm not using the In-Memory provider that ships with EF Core for this particular integration test.
My code for setting up the WebApplictionFactory is below:
public class CustomWebApplicationFactory<TStartup>
: WebApplicationFactory<TStartup> where TStartup : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder
.UseStartup<TStartup>()
.ConfigureServices(services =>
{
var sp = services.BuildServiceProvider();
// Create a scope to obtain a reference to the database context
using (var scope = sp.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var dbContext = scopedServices.GetRequiredService<MyDbContext>(); // <-- service not found
dbContext.Database.EnsureCreated();
new MyDbContextSeed()
.SeedAsync(dbContext)
.Wait();
}
});
}
}
Issue
The MyDbContext service isn't found, I understand why (I think) - because the ServiceProvider from my Startup.cs class hasn't built yet.
But the question is How can I access the services from my Startup class here?
For context, the Integration test looks like this:
public class MyAPITests
: IClassFixture<CustomWebApplicationFactory<Startup>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<Startup> _factory;
public MyAPITests(CustomWebApplicationFactory<Startup> factory)
{
_factory = factory;
_client = factory.CreateClient();
}
[Fact]
public async Task Get_ItemAsync_WhenIdNotFound_ReturnsNotFoundStatusCode()
{
// Arrange
var request = new HttpRequestMessage(HttpMethod.Get, "api/v1/item/0");
// Act
var response = await _client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
}
First, ConfigureServices can be used instead of UseStartup, not together. Second, you shouldn't create the scope and do the migration during ConfigureServices anyways, but rather after the Web Host is built, see here:
In older tutorials, you may see similar code in the Configure method
in Startup.cs. We recommend that you use the Configure method only to
set up the request pipeline. Application startup code belongs in the
Main method.
The only way to do this is not in the factory, but after the factory built it:
public class MyAPITests
: IClassFixture<CustomWebApplicationFactory<Startup>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<Startup> _factory;
public MyAPITests(CustomWebApplicationFactory<Startup> factory)
{
_factory = factory;
_client = factory.CreateClient();
var host = factory.Server.Host;
using (var scope = host.Services.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var dbContext = scopedServices.GetRequiredService<MyDbContext>();
dbContext.Database.EnsureCreated();
new MyDbContextSeed()
.SeedAsync(dbContext)
.Wait();
}
}
//...
}