Pass Ilogger on non-controller models - c#

I am creating an app in .net core and I'm trying to output a log.
I was able to log output from the controller and I also want to log output from the model, which is shown below.
Unfortunately, I don't know what to pass as arguments to the constructor. My guess is the same Ilogger<controller> as the controller, but I would like to know if there is a correct way.
Thanks in advance.
Controller:
public class SampleController : Controller {
public readonly ILogger<SampleController > _logger;
public SampleController (ILogger<SampleController > logger)
{
_logger = logger;
}
[HttpGet("[action]")]
public string FindSample()
{
_logger.LogInformation("FindSample Start");
// ***** Is it necessary to carry over the log of the sample controller to a much lower DAO from here? *****
var model = new SampleModel(_logger);
var result = model.Find();
_logger.LogInformation("FindSample End");
return result;
}
}
Model:
public class SampleModel
{
public readonly ILogger<SampleController> _logger;
public SampleModel(ILogger<SampleController> logger)
{
_logger = logger;
}
public string Find()
{
_logger.LogInformation("FindModel Start");
var dao = new SampleDao(_logger);
var code = dao.GetCode();
_logger.LogInformation("FindModel End");
return code;
}
}
Dao:
public class SampleDao
{
public readonly ILogger<SampleController> _logger;
public SampleContext SampleContext;
// ***** Should I pass it as an argument forever? *****
public SampleDao(ILogger<SampleController> logger)
{
_logger = logger;
if (SampleContext == null)
{
SampleContext = new SampleContext();
}
}
public string GetCode()
{
_logger.LogInformation("GetCode Start");
var code = SampleContext.SampleTable.FirstOrDefault().code;
_logger.LogInformation("GetCode End");
return code;
}
}

You should pass as a generic argument the class in which the logger is being injected. So you should use:
In your Dao
ILogger<SampleDao> logger
In your Model
ILogger<SampleModel> logger
For more information you could check:
Logging in ASP.NET core

Related

XUnit mocking a method but doesn't return correct result

I should be getting a list of Product Types but GetProductTypesAsync() returns a null.
Is the class ProductTypeRepo meant to be mocked since it calls the acutal API.
Anyone able to assist?
namespace UnitTest.Service
{
public class ProductTypeServiceTests
{
private readonly IServiceProvider _serviceProvider;
private readonly Mock<IProductTypeRepo> _productTypeRepoMock;
private readonly Mock<ILogger> _LoggerMock
private IProductTypeService _productTypeService;
public ProductTypeServiceTests()
{
_productTypeRepoMock = new Mock<IProductTypeRepo>();
_LoggerMock= new Mock<ILogger>();
_productTypeService = new ProductTypeService(_productTypeRepoMock.Object, _LoggerMock.Object);
}
[Fact]
public async Task GetProductType_ReturnOKStatusCode()
{
var serviceResponse = await _productTypeService.GetProductTypesAsync();
Assert.Equal(
expected: serviceResponse,
actual: serviceResponse
);
}
}
}
--
namespace Service.ProductType
{
public class ProductTypeService : IProductTypeService
{
private readonly IProductTypeRepo _repository;
private readonly ILogger _Logger;
public ProductTypeService(IProductTypeRepo repository, ILogger logger)
{
_repository = repository;
_logger = logger;
}
public async Task<List<Domain.DTO.ProductTypeResponse>> GetProductTypesAsync()
{
var productTypes = await _repository.GetProductTypesAsync();
if (productTypes == null)
{
throw new ProductTypeNotFoundException($"No Product Types were retrieved");
}
return productTypes;
}
}
}
xxxxxxxxxx xxxxxxxx xxxxxx xxxxx
Nowhere in the test is the mock configured to return anything when invoked.
//...
[Fact]
public async Task GetProductType_ReturnOKStatusCode() {
//Arrange
List<Domain.DTO.ProductTypeResponse> expected = new List<Domain.DTO.ProductTypeResponse>();
//..add items to expected list if necessary
_productTypeRepoMock
.Setup(_ => _.GetProductTypesAsync()) //<-- when this is invoked
.ReturnsAsync(expected); //<-- return something.
//Act
List<Domain.DTO.ProductTypeResponse> actual = await _productTypeService.GetProductTypesAsync();
//Assert
Assert.Equal(expected, actual);
}

How to prevent from "System.NotSupportedException : Unsupported expression" while mock ILogger LogInformation method?

ILogger's usage in my controller's method.
public class MetaDataController
{
private readonly ILogger<MetaDataController> _logger;
public MetaDataController(ILogger<MetaDataController> logger)
{
_logger = logger;
}
public async Task<IActionResult> GetSearchFields()
{
_logger.LogInformation("Received request for GetSearchFields");
..
..
}
}
my test case method
public class MetaDataControllerTests
{
private readonly MockRepository _mockRepository;
private readonly Mock<ILogger<MetaDataController>> _mockloggerController;
public MetaDataControllerTests()
{
this._mockRepository = new MockRepository(MockBehavior.Strict);
this._mockloggerController = this._mockRepository.Create<ILogger<MetaDataController>>();
}
private MetaDataController CreateMetaDataController()
{
return new MetaDataController(this._mockMetaDataService.Object, this._mockloggerController.Object);
}
[Fact()]
public async Task GetSearchFields_WhenCalled_ReturnsSearchOptionsMetaData()
{
//Arrange
//First attempt
_mockloggerController.Setup(x => x.LogInformation(It.IsAny<string>(), It.IsAny<object[]>()));
//Second attempt
_mockloggerController.Setup(x => x.LogInformation(It.IsAny<string>()));
/* at above line of code getting the exception message.*/
//Act
var metaDataController = this.CreateMetaDataController();
var result = await metaDataController.GetFeasibilitySearchFields().ConfigureAwait(false);
//Assert
}
}
as mention in /**/ comment getting the below exception
Message: System.NotSupportedException : Unsupported expression: x =>
x.LogInformation(It.IsAny(), new[] { }) Extension methods
(here: LoggerExtensions.LogInformation) may not be used in setup /
verification expressions.
so how to mock exactly and prevent this exception ?

Accessing property of Controller class from another class in C#

My code might be bit lengthy but easy to understand as I have tried my best to explain it, so please bare with me.
I have a controller class as:
public class QueryController : ControllerBase
{
private readonly ILogger<QueryController> log;
public bool ISReadCol { get; set; } //Focus on this part
public QueryController(ILogger<QueryController> logger)
{
this.log = logger;
}
[HttpGet("/api/v2/{database}/Tables/{table}")]
public async Task<IActionResult> GetColFields(
[FromRoute] string database,
[FromRoute] string table,
CancellationToken cancel)
{
//some code
ISReadCol = true; //setting property to true
return await GetQueryResult(database);
}
private async Task<IActionResult> GetQueryResult(string database)
{
//some code
return new QueryResult(pool, log); //[1]
}
}
Now, I want to access the property "ISReadCol" in the "QueryResult" class.
The "QueryResult.cs" is as follows:
class QueryResult : ActionResult
{
private readonly ILogger log;
private readonly ConnectionPoolEntry poolEntry;
public QueryResult(ConnectionPoolEntry poolEntry, ILogger log)
{
this.log = log;
this.poolEntry = poolEntry;
}
public override async Task ExecuteResultAsync(ActionContext context)
{
**//HOW CAN I ACCESS THE "ISReadCol" property here???.**
}
}
IF I pass "QueryController" instance in "QueryResult" constructor such as:
private readonly QueryController QR;
public QueryResult(ConnectionPoolEntry poolEntry, ILogger log, QueryController QR)
{
this.log = log;
this.poolEntry = poolEntry;
this.QR = QR;
}
and then QR.ISReadCol, but that doesn't work too as [1] call need to be updated too.
Just passed 'ISReadCol' as argument and modified the function calls accordingly

C# xUnit Test error - The following constructor parameters did not have matching fixture data

I have implemented a unit test on my ASP.NET Core MVC project using xUnit. When I try to run the test, it gave me an error as below:
The following constructor parameters did not have matching fixture data: Status status"
Below is my code:
IStatusService:
public interface IStatusService
{
Task<StatusIndexViewModel> GetStatusAsync();
}
StatusService:
public class StatusService : IStatusService
{
private readonly DbContext dbContext;
private readonly IMapper mapper;
public StatusService(DbContext dbContext, IMapper mapper)
{
this.dbContext = dbContext;
this.mapper = mapper;
}
public async Task<StatusIndexViewModel> GetStatusAsync()
{
var model = await dbContext
.Status.AsNoTracking()
.ProjectTo<StatusViewModel>(mapper.ConfigurationProvider)
.ToListAsync();
var vm = new StatusIndexViewModel
{
Statuses = model
};
return vm;
}
}
Here is the Controller:
public class StatusController : Controller
{
private readonly IStatusService statusService;
public StatusController(IStatusService statusService)
{
this.statusService = statusService;
}
public async Task<IActionResult> Index()
{
var model = await statusService.GetStatusAsync();
return View(model);
}
}
Below is my unit test class:
public class StatusUnitTest
{
StatusController StatusControllerTest;
private Mock<IStatusService> statusService;
private List<Status> statuses;
private Status status;
public StatusUnitTest(Status status)
{
this.status = status;
statusService = new Mock<IStatusService>();
statusService.Setup(p =>
p.GetStatusAsync()).ReturnsAsync(status);
StatusControllerTest = new StatusController(statusService.Object);
}
[Fact]
public async Task GetStatusByIdNo()
{
var result = await StatusControllerTest.Index();
var viewResult = Assert.IsType<ViewResult>(result);
Assert.IsAssignableFrom<IEnumerable<Status>>
(viewResult.ViewData.Model);
}
}
May I know what mistake I made? How I can test the controller and the service layer? Please give me a guide.

Add custom properties to telemetry request at controller level

I am trying to add specific properties to telemetry request for every route.
After digging a bit, I've found that I can create my own custom TelemetryInitializer by implementing ITelemetryInitializer.
By doing this I've managed to add global properties to the request.
However, I still need to add specific properties at the controller level.
Do you have any idea how can I achieve this?
I've tried to inject TelemetryClient into the controller, but if I use it the properties are shared between requests.
This is how I've tried to log in the controller:
private TelemetryClient telemetryClient;
public ValueController(TelemetryClient telemetryClient)
{
this.telemetryClient = telemetryClient;
}
[HttpGet]
public async Task<IActionResult> RouteOne([FromQuery(Name = "param1")]string param1, [FromQuery(Name = "param2")]string param2)
{
telemetryClient.Context.GlobalProperties["param1"] = param1;
telemetryClient.Context.GlobalProperties["param2"] = param2;
}
[HttpGet]
public async Task<IActionResult> RouteTwo([FromQuery(Name = "param3")]string param3, [FromQuery(Name = "param4")]string param4)
{
telemetryClient.Context.GlobalProperties["param3"] = param3;
telemetryClient.Context.GlobalProperties["param4"] = param4;
}
And this is the implementation of ITelemetryInitializer:
public class CustomPropertiesTelemetryInitializer : ITelemetryInitializer
{
private readonly IHttpContextAccessor httpContextAccessor;
public CustomPropertiesTelemetryInitializer(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public void Initialize(ITelemetry telemetry)
{
telemetry.Context.GlobalProperties["RequestId"] = httpContextAccessor.HttpContext.GetProperty("requestId");
telemetry.Context.GlobalProperties["Ip"] = httpContextAccessor.HttpContext?.Connection.RemoteIpAddress.ToString();
telemetry.Context.GlobalProperties["RoutePath"] = httpContextAccessor.HttpContext?.Request.Path;
}
}
If the properties you added are always like "paramxxx", then there is a workaround(but it's really not very elegant).
In the controller constructor, check the GlobalProperties if it contains key like "paramxxx":
public ValueController(TelemetryClient telemetryClient)
{
this.telemetryClient = telemetryClient;
var props = this.telemetryClient.Context.GlobalProperties;
foreach (var p in props)
{
if (p.Key.Contains("param"))
{
props.Remove(p.Key);
}
}
}
The key here is to use the DI framework. You can use it to get request-scoped data or services into your ITelemetryInitializer.
(These examples are based on the standard ASP.Net Dependency Injection framework. This pattern should work with any DI framework, but will need to be adjusted slightly.)
First, create a class to represent your request-scoped telemetry. I've used a simple DTO, but this could also be a service that knows how to fetch/generate the data itself. Register it using AddScoped. "Scoped" means that a new instance will be created for each HTTP request, and then that instance will be re-used within that request.
Because I used a DTO, I didn't bother with an interface--you should use an interface if the class contains any logic you'll want to mock in unit tests.
public class RequestScopedTelemetry
{
public string MyCustomProperty { get; set; }
}
services.AddScoped<RequestScopedTelemetry>();
Now, create the ITelemetryInitializer and register it as a singleton. App Insights will discover and use it through the DI framework.
class RequestScopedTelemetryInitializer : ITelemetryInitializer
{
readonly IHttpContextAccessor httpContextAccessor;
public RequestScopedTelemetryInitializer(IHttpContextAccessor httpContextAccessor)
=> this.httpContextAccessor = httpContextAccessor;
public void Initialize(ITelemetry telemetry)
{
// Attempt to resolve the request-scoped telemetry from the DI container
var requestScopedTelemetry = httpContextAccessor
.HttpContext?
.RequestServices?
.GetService<RequestScopedTelemetry>();
// RequestScopedTelemetry is only available within an active request scope
// If no telemetry available, just move along...
if (requestScopedTelemetry == null)
return;
// If telemetry was available, add it to the App Insights telemetry collection
telemetry.Context.GlobalProperties[nameof(RequestScopedTelemetry.MyCustomProperty)]
= requestScopedTelemetry.MyCustomProperty;
}
}
services.AddSingleton<ITelemetryInitializer, RequestScopedTelemetryInitializer>();
Finally, in your controller method, set your per-request values. This part isn't necessary if your telemetry class is able to fetch or generate the data itself.
public class ExampleController : ControllerBase
{
readonly RequestScopedTelemetry telemetry;
public ValuesController(RequestScopedTelemetry telemetry)
=> this.telemetry = telemetry;
[HttpGet]
public ActionResult Get()
{
telemetry.MyCustomProperty = "MyCustomValue";
// Do what you want to
return Ok();
}
}
In order to add per request data into telemetry, you need to have a way to share data within the request. A reliable way is by using HttpContent.Items property, which is basically a Dictionary.
You can create a service to keep a Dictionary inside HttpContent.Items with all custom data you want in telemetry (key prefix is used to ensure we only read the things we want later in Initializer):
public class LogTelemetryRequest
{
private const string KEY_PREFIX = "CustomTelemetryData_";
private readonly IHttpContextAccessor _httpContextAccessor;
public LogTelemetryRequest(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public void AddProperty(string key, string value)
{
_httpContextAccessor.HttpContext.Items[KEY_PREFIX + key] = value;
}
}
Register this as scoped in Startup.cs:
services.AddScoped<LogTelemetryRequest>();
Use it in your controller:
private LogTelemetryRequest logTelemetryRequest;
public ValueController(LogTelemetryRequest logTelemetryRequest)
{
this.logTelemetryRequest = logTelemetryRequest;
}
[HttpGet]
public async Task<IActionResult> RouteOne([FromQuery(Name = "param1")]string param1, [FromQuery(Name = "param2")]string param2)
{
// telemetryClient.Context.GlobalProperties["param1"] = param1;
// telemetryClient.Context.GlobalProperties["param2"] = param2;
logTelemetryRequest.AddProperty("param1", param1);
logTelemetryRequest.AddProperty("param2", param2);
}
Then read it within initializer:
public class AddCustomTelemetryInitializer : ITelemetryInitializer
{
private const string KEY_PREFIX = "CustomTelemetryData_";
private readonly IHttpContextAccessor _httpContextAccessor;
public AddCustomTelemetryInitializer(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public void Initialize(ITelemetry telemetry)
{
var requestTelemetry = telemetry as RequestTelemetry;
if (requestTelemetry == null) return;
foreach (var item in _httpContextAccessor.HttpContext.Items)
{
if (item.Key is string key && key.StartsWith(KEY_PREFIX))
requestTelemetry.Properties.Add(key, item.Value.ToString());
}
}
}
Ideally LogTelemetryRequest should be registered using an interface, and the key prefix should be a single shared constant, didn't do for the sake of simplicity.

Categories