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.
Related
I created a notification Event that I need to trigger within my post API after its completion.
The event Args are:
public class OrderCreationEvents : EventArgs
{
public string PickUpLocation { get; set; }
public string CustumerId { get; set; }
public OrderCreationEvents(string _pickUpLocation, string _custumerId)
{
PickUpLocation = _pickUpLocation;
CustumerId = _custumerId;
}
}
The notification Service interface is:
public interface IOrderCreatedService
{
void OnOrderCreation(object sender, OrderCreationEvents args);
}
The Interface's implementation is:
public class OrderNotificationService : IOrderCreatedService
{
private readonly IUnitOfWork _unitOfWork;
public OrderNotificationService(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async void OnOrderCreation(object sender, OrderCreationEvents args)
{
var note = new Notification { CustomerId = args.CustumerId, PickUpLocation = args.PickUpLocation };
await _unitOfWork.Notifications.Insert(note);
await _unitOfWork.Save();
}
}
Finally, when I create the post endpoint, I need to trigger this event
[HttpPost]
public async Task<IActionResult> Create(OrderIDTO orderIDTO)
{
var order = new Order {...};
await _unitOfWork.Orders.Insert(order);
//Calling the Notification Service
var note = new OrderNotificationService(_unitOfWork);
return RedirectToAction("index");
}
How do I trigger the OnOrderCreation mehtod inside the Create endpoint?
You also can follow Notification with MediatR which is really easy to use and understand.
This Link Can help to implement MediatR notification in your controller.
but, if you want to use MediatR notification in your scenario follow this:
public class OrderNotification : IAsyncNotification
{
public string PickUpLocation { get; set; }
public string CustumerId { get; set; }
}
public class OrderNotificationService : IAsyncNotificationHandler<OrderNotification>
{
private readonly IUnitOfWork _unitOfWork;
public OrderNotificationService(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task Handle(OrderNotification notification)
{
var note = new Notification { CustomerId = notification.CustumerId, PickUpLocation = notification.PickUpLocation };
await _unitOfWork.Notifications.Insert(note);
await _unitOfWork.Save();
}
}
and your controller :
private IMediator _mediator;
public constructor(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> Create(OrderIDTO orderIDTO)
{
var order = new Order { ...};
await _unitOfWork.Orders.Insert(order);
var note = new OrderNotification
{
CustomerId = order.CustomerId,
PickUpLocation = order.PickUpLocation
};
await _mediator.PublishAsync(note);
return RedirectToAction("index");
}
Remember before this, you need to install and setup the packages:
Assuming you have created an ASP.Net Core project in Visual Studio, the next step is installing the following NuGet packages.
MediatR
MediatR.Extensions.Microsoft.DependencyInjection
To do that, you can either use the NuGet Package Manager or the NuGet Package Manager Console.
now Configure MediatR in ASP.Net Core
Once the two packages mentioned in the earlier section have been successfully installed in your project, the next step is to configure MediatR in the Startup class. To do this, you should write the following code in the ConfigureServices method. Note that the ConfigureServices method is used to add services at runtime to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMediatR(typeof(Startup));
services.AddMvc().SetCompatibilityVersion
(CompatibilityVersion.Version_2_2);
}
I not 100% sure if i got what you want. but i think you do not want to use events. simply you can call the OnOrderCreation method like this:
var note = new OrderNotificationService(_unitOfWork);
await note.OnOrderCreation(this, new OrderCreationEvents {.....})
There is no argument given that corresponds to the required formal parameter 'userRoleService' of 'AuthorizeUserAttribute.AuthorizeUserAttribute(string, IUserRoleService, IModuleService, IUserService)'
AuthorizationController.cs
[AuthorizeUserAttribute("User.Edit")]
public ActionResult UserAuthorizationEdit()
AuthorizeUserAttribute.cs
public string Action { get; set; }
private IUserRoleService _userRoleService;
private IModuleService _moduleService;
private IUserService _userService;
public AuthorizeUserAttribute(IUserRoleService userRoleService, IModuleService moduleService, IUserService userService)
{
_userRoleService = userRoleService;
_moduleService = moduleService;
_userService = userService;
}
When I try to add constructor,controller side says write constructor as a parameter. How Can i change interface to a constructor
You need to use
[TypeFileter(typeof(AuthorizeUser),Arguments = new object[] { "User.Edit" }))]
public ActionResult UserAuthorizationEdit(int userId,
RoleRegisterDto authorizationModel)
in order to dependency injection can inject your services.
If you want to uses interfaces via class constructor using DI,you need to pass the parameter with the same type from custom attribute on controller side.
To avoid doing that, you could register your interfaces as services and get them using below code without constructor injection.For example:
1.Interface
public interface IUserRoleService
{
List<string> GetValues();
}
public class UserRoleService : IUserRoleService
{
private List<string> _privateList = new List<string>();
public List<string> GetValues()
{
_privateList.Add("test");
return _privateList;
}
}
2.In startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IUserRoleService, UserRoleService>();
}
3.Custom Authorization Attribute
public class AuthorizeUserAttribute:AuthorizeAttribute, IAsyncAuthorizationFilter
{
public string Action { get; set; }
public AuthorizeUserAttribute(string action)
{
Action = action;
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext authorizationFilterContext)
{
var x = authorizationFilterContext.HttpContext.RequestServices.GetRequiredService<IUserRoleService>();
var y = x.GetValues();
}
}
4.Action
[AuthorizeUserAttribute("User.Edit")]
public ActionResult UserAuthorizationEdit()
I created a ViewComponent to display a List<Product>, the list is valorized taken data from a REST API service, this is my class implementation:
public class ProductsViewComponent : ViewComponent
{
private readonly HttpClient _client;
public ProductsViewComponent(HttpClient client)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
}
public async Task<IViewComponentResult> InvokeAsync(string date)
{
using (var response = await _client.GetAsync($"/"product/get_products/{date}"))
{
response.EnsureSuccessStatusCode();
var products = await response.Content.ReadAsAsync<List<Product>>();
return View(products);
}
}
}
I load the List inside an html table which is available inside the Components folder: Views\Shared\Components\Products\Default.cshtml.
In each View that needs to display the Products I did:
#await Component.InvokeAsync("Products", new { date = myDate })
The REST API is called using the HttpClient configured in the Startup.cs as following:
services.AddHttpClient<ProductsViewComponent>(c =>
{
c.BaseAddress = new Uri('https://api.myservice.com');
});
This works well, but the main problem is each time the user reload the page or maybe go inside another View which require to display the list of products, then the app will make another API call.
Is possible store the list in something like a cache and prevent to call the API again if the date is equal than the previous date selected?
I'm learning ASP.NET Core so I'm not really expert on this argument.
Thanks in advance for any help.
As per microsoft documentation https://learn.microsoft.com/en-us/aspnet/core/performance/caching/memory?view=aspnetcore-2.1
you can use IMemoryCache to cache data
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMemoryCache();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
public void Configure(IApplicationBuilder app)
{
app.UseMvcWithDefaultRoute();
}
}
and create instance of IMemoryCache. This is an example from Microsoft documentation. You can Create another class to handle this all together and In below example this is just saving DateTime But, you can save any object in cache and when you try to read that value from cache just need to cast that object into a Type.
I will strongly recommend you go through the above documentation.
public class HomeController : Controller
{
private IMemoryCache _cache;
public HomeController(IMemoryCache memoryCache)
{
_cache = memoryCache;
}
public IActionResult CacheTryGetValueSet()
{
DateTime cacheEntry;
// Look for cache key.
if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
{
// Key not in cache, so get data.
cacheEntry = DateTime.Now;
// Set cache options.
var cacheEntryOptions = new MemoryCacheEntryOptions()
// Keep in cache for this time, reset time if accessed.
.SetSlidingExpiration(TimeSpan.FromSeconds(3));
// Save data in cache.
_cache.Set(CacheKeys.Entry, cacheEntry, cacheEntryOptions);
}
return View("Cache", cacheEntry);
}
}
Update: CacheKeys.Entry is a static class where all keys are defined. (Just coding standards). Please check the above documentation link.
public static class CacheKeys
{
public static string Entry { get { return "_Entry"; } }
public static string CallbackEntry { get { return "_Callback"; } }
public static string CallbackMessage { get { return "_CallbackMessage"; } }
public static string Parent { get { return "_Parent"; } }
public static string Child { get { return "_Child"; } }
public static string DependentMessage { get { return "_DependentMessage";} }
public static string DependentCTS { get { return "_DependentCTS"; } }
public static string Ticks { get { return "_Ticks"; } }
public static string CancelMsg { get { return "_CancelMsg"; } }
public static string CancelTokenSource { get { return "_CancelTokenSource";} }
}
You can use a distributed cache and so use Redis for example with a ConnectionMultiplexer.
And so foreach call you can call your redis for the cache which is implement thanks to an interface call here 'IDistributedCache'
You can find a lot of documentation to implement cache and use it.
: .Net framework
DotNet Core
Your controller X :
[HttpGet]
[Route("{itemId}")]
public async Task<IHttpActionResult> GetItemById(int eventId, [FromUri]EventTabs tabId)
{
ServiceResponse<ItemDto> result = await _itemDispatcher.GetItemById(itemId);
return WrapResponse(result);
}
Your dispatcher to get the item by id which use redis cache (already implement)
public class ItemDispatcher : ItemDispatcher
{
private readonly IUnitOfWork _unitOfWork;
private readonly IDistributedCache _distributedCache; // use interface of your implementation of redis cache
private readonly int _cacheDuration;
private readonly bool _isCacheEnabled;
public EventDispatcher(IUnitOfWork unitOfWork, IDistributedCache distCache)
{
_unitOfWork = unitOfWork;
_distributedCache = distCache; // init cache in constructor
_cacheDuration = _configuration.Get<int>("cache.duration"); // duration of your cache
_isCacheEnabled = _configuration.Get<bool>("cache.isEnable"); // if the cache is enable or not
}
public async Task<ServiceResponse<ItemDto>> GetItemById(int id)
{
// Add this for each Task call
var cacheKey = string.Empty;
if (_isCacheEnabled)
{
cacheKey = CacheUtils.GetCacheKey(CacheKeys.Item, id);
itemDto cacheResult = await _distributedCache.Get<ItemDto>(cacheKey);
if (cacheResult != null)
return new ServiceResponse<Item>(cacheResult);
}
}
Try This
Cache["KeyName"] = VariableOrTable; Cache.Insert("Key", VariableOrTable, null,
Cache.NoAbsoluteExpiration, ts);
I have an ASP.Net Core 2 Web application.
I'm trying to create a custom routing Middleware, so I can get the routes from a database.
In ConfigureServices() I have:
services.AddDbContext<DbContext>(options =>
options.UseMySQL(configuration.GetConnectionString("ConnectionClient")));
services.AddScoped<IServiceConfig, ServiceConfig>();
In Configure():
app.UseMvc(routes =>
{
routes.Routes.Add(new RouteCustom(routes.DefaultHandler);
routes.MapRoute(name: "default", template: "{controller=Home}/{action=Index}/{id?}");
});
In the RouteCustom
public class RouteCustom : IRouteCustom
{
private readonly IRouter _innerRouter;
private IServiceConfig _serviceConfig;
public RouteCustom(IRouter innerRouter)
{
_innerRouter = innerRouter ?? throw new ArgumentNullException(nameof(innerRouter));
}
public async Task RouteAsync(RouteContext context)
{
_serviceConfig = context.HttpContext
.RequestServices.GetRequiredService<IServiceConfig>();
/// ...
// Operations inside _serviceConfig to get the route
}
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
_serviceConfig = context.HttpContext
.RequestServices.GetRequiredService<IServiceConfig>();
// ...
// Operations inside _serviceConfig to get the route
}
}
The IServiceConfig it is just a class where I access the database to get data, in this case the routes, but also other configuration data I need for the application.
public interface IServiceConfig
{
Config GetConfig();
List<RouteWeb> SelRoutesWeb();
}
public class ServiceConfig : IServiceConfig
{
private readonly IMemoryCache _memoryCache;
private readonly IUnitOfWork _unitOfWork;
private readonly IServiceTenant _serviceTenant;
public ServiceConfig(IMemoryCache memoryCache,
IUnitOfWork unitOfWork,
IServiceTenant serviceTenant)
{
_memoryCache = memoryCache;
_unitOfWork = unitOfWork;
_serviceTenant = serviceTenant;
}
public Config GetConfig()
{
var cacheConfigTenant = Names.CacheConfig + _serviceTenant.GetId();
var config = _memoryCache.Get<Config>(cacheConfigTenant);
if (config != null)
return config;
config = _unitOfWork.Config.Get();
_memoryCache.Set(cacheConfigTenant, config,
new MemoryCacheEntryOptions()
{
SlidingExpiration = Names.CacheExpiration
});
return config;
}
public List<RouteWeb> SelRoutesWeb()
{
var cacheRoutesWebTenant = Names.CacheRoutesWeb + _serviceTenant.GetId();
var routesWebList = _memoryCache.Get<List<RouteWeb>>(cacheRoutesWebTenant);
if (routesWebList != null)
return routesWebList;
routesWebList = _unitOfWork.PageWeb.SelRoutesWeb();
_memoryCache.Set(cacheRoutesWebTenant, routesWebList,
new MemoryCacheEntryOptions()
{
SlidingExpiration = Names.CacheExpiration
});
return routesWebList;
}
}
The problem is I'm getting this message when I test with multiple tabs opened and try to refresh all at the same time:
"A second operation started on this context before a previous operation completed"
I'm sure there is something I'm doing wrong, but I don't know what. It has to be a better way to access the db inside the custom route middleware or even a better way for doing this.
For example, on a regular Middleware (not the routing one) I can inject the dependencies to the Invoke function, but I can't inject dependencies here to the RouteAsync or the GetVirtualPath().
What can be happening here?
Thanks in advance.
UPDATE
These are the exceptions I'm getting.
An unhandled exception occurred while processing the request.
InvalidOperationException: A second operation started on this context before a previous operation completed. Any instance members are not guaranteed to be thread safe.
And this one:
An unhandled exception occurred while processing the request.
MySqlException: There is already an open DataReader associated with this Connection which must be closed first.
This is the UnitOfWork
public interface IUnitOfWork : IDisposable
{
ICompanyRepository Company { get; }
IConfigRepository Config { get; }
// ...
void Complete();
}
public class UnitOfWork : IUnitOfWork
{
private readonly DbContext _context;
public UnitOfWork(DbContext context)
{
_context = context;
Company = new CompanyRepository(_context);
Config = new ConfigRepository(_context);
// ...
}
public ICompanyRepository Company { get; private set; }
public IConfigRepository Config { get; private set; }
// ...
public void Complete()
{
_context.SaveChanges();
}
public void Dispose()
{
_context.Dispose();
}
}
UPDATE 2
After reviewing the comments and making a lot of tests, the best clue I have is when I remove the CustomRoute line the problem disappear. Removing this line from Configure function on Startup.cs
routes.Routes.Add(new RouteCustom(routes.DefaultHandler));
Also I have tried removing, first the RouteAsync and then the GetVirtualPath() methods, but if one of those is present I get an error, so it is clear that the problem is in this CustomRoute class.
In the TenantMiddleware, which is called first for any request, I'm injecting the UnitOfWork and I have no problem. This Middleware is create in the Configure function:
app.UseMiddleware<TenantMiddleware>();
And inside, I'm injecting the UnitOfWork, and using it on every request, like this:
public async Task Invoke(HttpContext httpContext, IServiceTenant serviceTenant)
{
// ...performing DB operations to retrieve the tenent's data.
}
public class ServiceTenant : IServiceTenant
{
public ServiceTenant(IHttpContextAccessor contextAccessor,
IMemoryCache memoryCache,
IUnitOfWorkMaster unitOfWorkMaster)
{
_unitOfWorkMaster = unitOfWorkMaster;
}
// ...performing DB operations
}
SO, the problem with the CustomRoute is I can't inject the dependencies by adding to the Invoke function like this:
public async Task Invoke(HttpContext httpContext, IServiceTenant serviceTenant)
So I have to call the corresponding Service (Inside that service I inject the UnitOfWork and perform the DB operations) like this, and I think this can be the thing that is causing problems:
public async Task RouteAsync(RouteContext context)
{
_serviceConfig = context.HttpContext
.RequestServices.GetRequiredService<IServiceConfig>();
// ....
}
because this is the only way I know to "inject" the IServiceConfig into the RouteAsync and GetVirtualPath()...
Also, I'm doing that in every controller since I'm using a BaseCOntroller, so I decide which os the injection services I use...
public class BaseWebController : Controller
{
private readonly IMemoryCache _memoryCache;
private readonly IUnitOfWork _unitOfWork;
private readonly IUnitOfWorkMaster _unitOfWorkMaster;
private readonly IServiceConfig _serviceConfig;
private readonly IServiceFiles _serviceFiles;
private readonly IServiceFilesData _serviceFilesData;
private readonly IServiceTenant _serviceTenant;
public BaseWebController(IServiceProvider serviceProvider)
{
_memoryCache = serviceProvider.GetRequiredService<IMemoryCache>();
_unitOfWork = serviceProvider.GetRequiredService<IUnitOfWork>();
_unitOfWorkMaster = serviceProvider.GetRequiredService<IUnitOfWorkMaster>();
_serviceConfig = serviceProvider.GetRequiredService<IServiceConfig>();
_serviceFiles = serviceProvider.GetRequiredService<IServiceFiles>();
_serviceFilesData = serviceProvider.GetRequiredService<IServiceFilesData>();
_serviceTenant = serviceProvider.GetRequiredService<IServiceTenant>();
}
}
And then in every controller, instead of referencing all of the injected services, I can do it only for those I need, like this:
public class HomeController : BaseWebController
{
private readonly IUnitOfWork _unitOfWork;
public HomeController(IServiceProvider serviceProvider) : base(serviceProvider)
{
_unitOfWork = serviceProvider.GetRequiredService<IUnitOfWork>();
}
public IActionResult Index()
{
// ...
}
}
I don't know if this has something to do with my problem, but I'm just showing you what I think can be the problem, so you can have more information.
Thanks.
UPDATE 3
This is the code of the db to retrieve the routes:
public class PageWebRepository : Repository<PageWeb>, IPageWebRepository
{
public PageWebRepository(DbContext context) : base(context) { }
public List<RouteWeb> SelRoutesWeb()
{
return Context.PagesWebTrs
.Include(p => p.PageWeb)
.Where(p => p.PageWeb.Active)
.Select(p => new RouteWeb
{
PageWebId = p.PageWebId,
LanguageCode = p.LanguageCode,
Route = p.Route,
Regex = p.PageWeb.Regex.Replace("<route>", p.Route),
Params = p.PageWeb.Params,
Area = p.PageWeb.Area,
Controller = p.PageWeb.Controller,
Action = p.PageWeb.Action,
Type = p.PageWeb.Type,
Sidebar = p.PageWeb.Sidebar,
BannerIsScript = p.PageWeb.BannerIsScript,
Title = p.Title,
Description = p.Description,
Keywords = p.Keywords,
ScriptHead = p.ScriptHead,
ScriptBody = p.ScriptBody,
BannerScript = p.BannerScript,
BannerUrl = p.BannerUrl,
})
.ToList();
}
}
Where PagesWebTrs are the translations of the pages (multi language) and PagesWeb is the main table.
This issue is indeed within the route middleware.
Per definition, a middleware is a singleton, so a single instance handles all requests. This results into the instance state (the IServiceConfigwith hooked up DbContext) being accessed and changed by multiple simultaneous requests; it's a well disguished classical concurrency issue.
An example.
Request A executes RouteAsync, sets the _serviceConfig and executes a query on the DbContext. Nano seconds (or less :)) later, request B does the same. While request B's query is being executed, request A executes GetVirtualPath, but this time on the DbContext set by request B. This results in a second query being executed on the DbContext of request B which still has one running and you get the mentionned error.
The solution is to prevent shared state, by retrieving the IServiceConfig at the start of each method.
As you already said, getting such a dependency injected via the Invoke method does not work; the Invokemethod does not get executed.
Here below is the reworked RouteCustom.
public class RouteCustom : IRouteCustom
{
private readonly IRouter _innerRouter;
public RouteCustom(IRouter innerRouter)
{
_innerRouter = innerRouter ?? throw new ArgumentNullException(nameof(innerRouter));
}
public async Task RouteAsync(RouteContext context)
{
var serviceConfig = context.HttpContext.RequestServices.GetRequiredService<IServiceConfig>();
// ...
}
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
var serviceConfig = context.HttpContext.RequestServices.GetRequiredService<IServiceConfig>();
// ...
}
}
I'm creating an Bot Application using Luis and Luis Action Binding like the following sample
Luis action binding sample on GitHub
I want to use dependency injection to pass the data service instance to the action binding
I add a data layer following repository design pattern like that:
public class ProviderService : IProviderService
{
private readonly IProviderRepository ProviderRepository;
private readonly IUnitOfWork UnitOfWork;
public ProviderService(IProviderRepository providerService, IUnitOfWork unitOfWork)
{
this.ProviderRepository = providerService;
this.UnitOfWork = unitOfWork;
}
public void CreateProvider(Provider provider)
{
ProviderRepository.Add(provider);
}
and will use it from the fulfill method in the action binding class like that
public class FindNumberAction : BaseLuisAction
{
IProviderService _provider;
[Required(ErrorMessage = "Please provide a ProviderName")]
public string Provider { get; set; }
public string ProviderType { get; set; }
public FindNumberAction() : this(new ProviderService(new ProviderRepository(new DbFactory()), new UnitOfWork(new DbFactory())))
{
}
public FindNumberAction(IProviderService provider)
{
_provider = provider;
}
public override Task<object> FulfillAsync()
{
var providerData = _provider.GetByProviderName(Provider);
if (!string.IsNullOrEmpty(providerData.ProviderAddress))
{
var result = new ProviderInfo
{
ProviderAddress = providerData.ProviderAddress,
ProviderName = providerData.Provider.ProviderName,
ProviderPhone = providerData.ProviderPhone,
ProviderType = providerData.Provider.ProviderType.ProviderTypeName
};
return Task.FromResult((object)result.GetNumber());
}
return Task.FromResult((object)new ProviderInfo() { ProviderName = Provider, ProviderType = ProviderType }.NoProviderNumberFound());
}
}
https://github.com/Microsoft/BotBuilder/tree/master/CSharp/Samples/AlarmBot
Here is a great example of how to implement AutoFac in the bot framework. You'll see that you not only need to implement the container, but also have to change the way the MessagesController handles the incomming messages.