I am trying to make communication between microservices using eventbus, when I use dependency injection my controller can no longer be called.
I have my Controller
public class CustomersController : ControllerBase
{
private readonly ICustomerRepository _customerRepository;
private readonly IIdentityService _identityService;
private readonly ICustomerIntegrationEventService _customerIntegrationEventService;
public CustomersController(ICustomerRepository customerRepository, IIdentityService identityService, ICustomerIntegrationEventService customerIntegrationEventService)
{
_customerRepository = customerRepository ?? throw new ArgumentNullException(nameof(customerRepository));
_identityService = identityService ?? throw new ArgumentNullException(nameof(identityService));
_customerIntegrationEventService = customerIntegrationEventService ?? throw new ArgumentNullException(nameof(customerIntegrationEventService));
}
}
In this Controller, I have a method named Add. It basically adds a client. When client is added, I would like to notify other microservice and send the data to a service bus. So far I'm using Integration Event. But in the moment that the dependency injection is done in the controller. the front can no longer hit the controller, returning an error 500.
public async Task<IActionResult> Add(Customer value)
{
var idAdded = await _customerRepository.Add(value).ConfigureAwait(false);
if (!idAdded.HasValue)
return BadRequest();
var integrationEvent = new CustomerIntegrationEvent(idAdded.Value, value);
await _customerIntegrationEventService.AddAndSaveEventAsync(integrationEvent);
return CreatedAtAction(nameof(Get), new { id = idAdded.Value }, null);
}
Soon below is how this class building _customerIntegrationEventService
CustomerIntegrationEventService
public class CustomerIntegrationEventService : ICustomerIntegrationEventService
{
private readonly Func<DbConnection, IIntegrationEventLogService> _integrationEventLogServiceFactory;
private readonly IEventBus _eventBus;
private readonly ApplicationDataContext _osDataContext;
private readonly IntegrationEventLogContext _eventLogContext;
private readonly IIntegrationEventLogService _eventLogService;
public CustomerIntegrationEventService(
IEventBus eventBus,
ApplicationDataContext hrDataContext,
IntegrationEventLogContext eventLogContext,
Func<DbConnection, IIntegrationEventLogService> integrationEventLogServiceFactory)
{
_osDataContext = hrDataContext ?? throw new ArgumentNullException(nameof(hrDataContext));
_eventLogContext = eventLogContext ?? throw new ArgumentNullException(nameof(eventLogContext));
_integrationEventLogServiceFactory = integrationEventLogServiceFactory ?? throw new ArgumentNullException(nameof(integrationEventLogServiceFactory));
_eventBus = eventBus ?? throw new ArgumentNullException(nameof(eventBus));
_eventLogService = _integrationEventLogServiceFactory(hrDataContext.Database.GetDbConnection());
}
public async Task PublishEventsThroughEventBusAsync()
{
var pendindLogEvents = await _eventLogService.RetrieveEventLogsPendingToPublishAsync();
foreach (var logEvt in pendindLogEvents)
{
try
{
await _eventLogService.MarkEventAsInProgressAsync(logEvt.EventId);
_eventBus.Publish(logEvt.IntegrationEvent);
await _eventLogService.MarkEventAsPublishedAsync(logEvt.EventId);
}
catch (Exception)
{
await _eventLogService.MarkEventAsFailedAsync(logEvt.EventId);
}
}
}
public async Task AddAndSaveEventAsync(IntegrationEvent evt)
{
await _eventLogService.SaveEventAsync(evt, _osDataContext.Database.CurrentTransaction.GetDbTransaction());
}
}
all of these codes were taken from the example https://learn.microsoft.com/en-us/dotnet/standard/microservices-architecture/multi-container-microservice-net-applications/subscribe-events
I made the dependency injection in the startup, but anyway the error persists
public void AddIntegrationServices(IServiceCollection services, IConfiguration configuration)
{
services.AddTransient<Func<DbConnection, IIntegrationEventLogService>>(
sp => (DbConnection c) => new IntegrationEventLogService(c));
services.AddTransient<ICustomerIntegrationEventService, CustomerIntegrationEventService>();
}
How could I at least see the error behind things, or how I come up with that solution. This code is based on microsoft eShopOnContainers
Related
I have a WebAPI server with integrated SignalR Hubs.
The problem it is in the integration between both components and efficiently call the clients interested on a given item that was updated through REST with the least overhead possible on the Controller side.
I have read about Background tasks with hosted services in ASP.NET Core, or Publish Subscriber Patterns but they don't seem the right fit for this problem.
From the documentation examples, the background tasks seem to atempt to preserve order which is not required, in fact, it is desired to allow multiple requests to be handled concurrently, as efficiently as possible.
With this in mind, I created this third component called MappingComponent that is being called through a new Task.
It is important to design the Controller in a way that he spends the least amount of work "raising the events" possible. Exceptions should be (i believe) handled within the MappingComponent.
What would be a better approach/design pattern that the following implementation, to avoid using Task.Run?
ApiController
[Route("api/[controller]")]
[ApiController]
public class ItemController : ControllerBase
{
private readonly MappingComponent mappingComponent;
private readonly IDataContext dataContext;
[HttpPost]
public async Task<ActionResult<Item>> PostItem(ItemDTO itemDTO)
{
await dataContext.Items.AddAsync(item);
(...)
_ = Task.Run(async () =>
{
try
{
await mappingComponent.NotifyOnItemAdd(item);
}
catch (Exception e)
{
Console.WriteLine(e);
}
});
return CreatedAtAction("Get", new { id = item.Id }, item);
}
[HttpDelete("{id}", Name = "Delete")]
public async Task<IActionResult> Delete(int id)
{
var item = await dataContext.Items.FindAsync(id);
(...)
_ = Task.Run(async () =>
{
try
{
await mappingComponent.NotifyOnItemDelete(item);
}
catch (Exception e)
{
Console.WriteLine(e);
}
});
return NoContent();
}
[HttpPatch("{id}", Name = "Patch")]
public async Task<IActionResult> Patch(int id,
[FromBody] JsonPatchDocument<Item> itemToPatch)
{
var item = await dataContext.Items.FindAsync(id);
(...)
_ = Task.Run(async () =>
{
try
{
await mappingComponent.NotifyOnItemEdit(item);
}
catch (Exception e)
{
Console.WriteLine(e);
}
});
return StatusCode(StatusCodes.Status202Accepted);
}
}
SignalR Hub
public class BroadcastHub : Hub<IHubClient>
{
private readonly MappingComponent mappingComponent;
public BroadcastHub(MappingComponent mappingComponent)
{
this.mappingComponent = mappingComponent;
}
public override Task OnConnectedAsync()
{
mappingComponent.OnConnected(Context.User.Identity.Name, Context.ConnectionId));
return base.OnConnectedAsync();
}
public override Task OnDisconnectedAsync()
{
mappingComponent.OnDisconnected(Context.User.Identity.Name, Context.ConnectionId));
return base.OnDisconnectedAsync();
}
public void Subscribe(string itemQuery)
{
mappingComponent.SubscribeConnection(Context.User.Identity.Name, Context.ConnectionId, itemQuery));
}
public void Unsubscribe()
{
mappingComponent.UnsubscribeConnection(Context.ConnectionId));
}
}
"MappingComponent" being registered as singleton on startup
public class MappingComponent
{
private readonly IServiceScopeFactory scopeFactory;
private readonly IHubContext<BroadcastHub, IHubClient> _hubContext;
private static readonly ConcurrentDictionary<string, User> Users = new(StringComparer.InvariantCultureIgnoreCase);
private static readonly ConcurrentDictionary<int, List<string>> ItemConnection = new();
private static readonly ConcurrentDictionary<string, List<int>> ConnectionItem = new();
public MappingComponent(IServiceScopeFactory scopeFactory, IHubContext<BroadcastHub, IHubClient> hubContext)
{
//this.dataContext = dataContext;
this._hubContext = hubContext;
this.scopeFactory = scopeFactory;
}
internal void OnConnected(string userName, string connectionId){(...)}
internal void OnDisconnected(string userName, string connectionId){(...)}
internal void SubscribeConnection(string userName, string connectionId, string query){(...)}
internal void UnsubscribeConnection(string connectionId){(...)}
internal async Task NotifyOnItemAdd(Item item)
{
List<string> interestedConnections = new();
(...)
//Example containing locks
lock()
{
//There is a need to acess EF database
using (var scope = scopeFactory.CreateScope())
{
var dataContext = scope.ServiceProvider.GetRequiredService<IDataContext>()
await dataContext.Items.(...)
interestedConnections = ...
}
}
await _hubContext.Clients.Clients(interestedConnections).BroadcastItem(item);
}
internal async Task NotifyOnItemEdit(Item item)
{
List<string> interestedConnections = new();
(...)
await _hubContext.Clients.Clients(interestedConnections).BroadcastItem(item);
}
internal async Task NotifyOnItemDelete(Item item)
{
List<string> interestedConnections = new();
(...)
await _hubContext.Clients.Clients(interestedConnections).BroadcastAllItems();
}
}
Description
I want to create an object of a class with dependency injection. If I set the parameter manually I got the exception Cannot access a disposed of the object..
This Application is a Blazor wasm with Dotnet core 3.1. I´ve created a Middleware that should connect to a query console. So I have a static list that contains all query clients. If a client is missing it will be created.
Invoke Async in the middleware:
public async Task InvokeAsync(HttpContext context,
IConfiguration configuration,
IInstanceControlRepository instanceControlRepository,
IServiceProvider serviceProvider)
{
_configuration = configuration;
_instanceControlRepository = instanceControlRepository;
long timestamp = new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds();
var instances = _instanceControlRepository.GetAllInstances();
if (_time + 3 <= timestamp)
{
_time = timestamp;
// Remove
foreach(var client in TeamspeakInstanceQueryClients.ToList())
{
var cl = instances.ToList().Find(el => el.Id == client.Instance.Id);
if(cl == null)
{
client.Dispose();
TeamspeakInstanceQueryClients.RemoveAll(el => el.Instance.Equals(client.Instance));
}
}
// Create & Update
foreach (var instance in instances)
{
var queryClient = TeamspeakInstanceQueryClients.Find(el => el.Instance.Id == instance.Id);
if(queryClient == null)
{
//var test = ActivatorUtilities.CreateInstance<ApplicationDbContext>(serviceProvider);
//var dbContext = serviceProvider.GetService<ApplicationDbContext>();
//queryClient = new TeamspeakInstanceQueryClient(new InstancesControlRepository(ActivatorUtilities.CreateInstance<ApplicationDbContext>(serviceProvider)));
queryClient = new TeamspeakInstanceQueryClient(serviceProvider);
_ = queryClient.Connect(instance);
TeamspeakInstanceQueryClients.Add(queryClient);
}
else
{
_ = queryClient.CheckInstanceData(instance);
}
}
}
await _next(context);
}
TeamspeakInstanceQueryClient.cs
public partial class TeamspeakInstanceQueryClient : ITeamspeakInstanceQueryClient
{
private IInstanceControlRepository _instanceControlRepository;
private const short MAX_RETRYS = 3;
private const short TIME_TO_RETRY = 10;
private EventHandler OnConnected;
public Instance Instance { get; internal set; }
public TeamSpeakClient Client { get; internal set; }
public bool IsSelected { get; internal set; }
private short _connectionTrys = 0;
public TeamspeakInstanceQueryClient(IServiceProvider serviceProvider)
{
_instanceControlRepository = new InstancesControlRepository(ActivatorUtilities.CreateInstance<ApplicationDbContext>(serviceProvider));
Init();
}
}
InstancesControlRepository.cs
public class InstancesControlRepository : IInstanceControlRepository
{
private readonly ApplicationDbContext _applicationDbContext;
public InstancesControlRepository(ApplicationDbContext applicationDbContext)
{
_applicationDbContext = applicationDbContext;
}
}
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(option =>
option.UseMySql(
Configuration.GetConnectionString("DefaultConnection"),
mySqlOptions => mySqlOptions.ServerVersion(new System.Version(10, 4, 13), ServerType.MariaDb)
)
);
services.AddScoped<IInstanceControlRepository, InstancesControlRepository>();
services.AddScoped<IServerQueryRepository, ServerQueryRepository>();
What I´ve tried
I´ve tried to create the class with the service provider but it comes to the same error
I´ve tried to create the missing parameters with the service provider in the created class. But I need to inject the service provider which also creates the exception Cannot access a disposed of the object. Object name: 'IServiceProvider'.
I´ve tried to make the service provider static so it can´t be disposed but it is disposed.
It seems that instance of IServiceProvider is a scoped one and it is disposed when the scope ends (in the end of request I assume). You can try define singleton factory for your TeamspeakInstanceQueryClient and use it:
class ClientFactory
{
private IServiceProvider _sp { get; set; }
private IServiceScope _scope { get; set; }
public MyClass(IServiceProvider sp)
{
_sp = sp;
_scope = sp.CreateScope();
}
public TeamspeakInstanceQueryClient Create() => new TeamspeakInstanceQueryClient(_scope.ServiceProvider);
}
// register it as singleton
services.AddSingleton<ClientFactory>();
and use it in InvokeAsync:
var factory = serviceProvider.GetRequiredService<ClientFactory>();
queryClient = factory.Create();
P.S. this code can be improved vastly and is used only for demonstration purposes.
I am trying to implement an asp.net web API and it is my first time trying to implement such a project. I googled a lot.
Many times lost down the road because there is a lot of misguiding articles. Now I accomplished to implement the layered architecture. I have a business entity, business service, data models class library projects and a web API itself. As I said I googled a lot since I have no experience with web API, I decided to use generic repository and unit of work in my solution. My API is working butI get the following error when I load test with 400 users:
System.Transactions.TransactionException: The operation is not valid for the state of the transaction. ---> System.TimeoutException: Transaction Timeout. The underlying provider failed on Open.
I researched and asked experienced friends, they said I am not disposing of all the DBContexts I am creating. As I am using transient scope, a new DBcontext is created for every action call (controller construction), but unless I call context.Dispose() in the action the pools are not released.
Honestly, I am not sure how can I dispose of more.
Controller:
namespace Game.Controllers
{
[System.Web.Http.RoutePrefix("api/v2/game")]
public class GameController : ApiController
{
private readonly IGameServices _gameServices;
#region Public Constructor
public GameController(IGameServices gameServices)
{
_gameServices = gameServices;
}
#endregion
[HttpPost, Route("purchase")]
public async Task<IHttpActionResult> PurchaseGame(RequestDto game)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
//Call Services
var gameConfirmResponse = await _gameServices.GamePurchase(game);
switch (gameConfirmResponse.StatusCode)
{
case HttpStatusCode.NotFound:
{
return NotFound();
}
case HttpStatusCode.InternalServerError:
{
return InternalServerError();
}
case HttpStatusCode.OK:
{
if (gameConfirmResponse.Content == null)
{
return new System.Web.Http.Results.ResponseMessageResult(
Request.CreateErrorResponse((HttpStatusCode) 222, new HttpError("No Results Found")));
}
var responseStream = await gameConfirmResponse.Content.ReadAsStringAsync();
var resultResponse = JsonConvert.DeserializeObject<GameConfirmResponse>(responseStream);
if (resultResponse.coupons == null)
{
return new System.Web.Http.Results.ResponseMessageResult(
Request.CreateErrorResponse((HttpStatusCode) 222,
new HttpError("No Coupons Available for this Game")));
}
//Transform GameConfirmResponse into DTO for returning result
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<GameConfirmResponse, GameConfirmResponseDto>();
cfg.CreateMap<Coupon, CouponDto>();
});
var iMapper = config.CreateMapper();
var resultDto = iMapper.Map<GameConfirmResponse, GameConfirmResponseDto>(resultResponse);
return Ok(resultDto);
}
case HttpStatusCode.Unauthorized:
{
return Unauthorized();
}
case HttpStatusCode.RequestTimeout:
{
return InternalServerError();
}
}
return Ok(gameConfirmResponse);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
}
base.Dispose(disposing);
}
}
}
Service:
namespace BusinessService
{
public class GameServices : IGameServices, IDisposable
{
private readonly UnitOfWork _unitOfWork;
public GameServices(UnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task<HttpResponseMessage> GamePurchase(RequestDto requestDto)
{
HttpResponseMessage response = null;
response = await CallRazerService(requestDto);
return response;
}
private async Task<HttpResponseMessage> CallRazerService(RequestDto requestDto)
{
HttpResponseMessage response = null;
using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
//Added to see if I fix the error
TransactionInterop.GetTransmitterPropagationToken(Transaction.Current);
//Transform DTO into GameRequest for calling Razer Initiate
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<RequestDto, GameRequest>();
cfg.CreateMap<GameRequest, GameConfirmRequest>();
cfg.CreateMap<GameConfirmResponse, GameConfirmResponseDto>();
cfg.CreateMap<Coupon, CouponDto>();
cfg.CreateMap<GameRequest, GameRequestDto>();
});
var iMapper = config.CreateMapper();
var gameRequest = iMapper.Map<RequestDto, GameRequest>(requestDto);
//Unique reference ID
gameRequest.referenceId = Guid.NewGuid();
var gameRequestDto = iMapper.Map<GameRequest, GameRequestDto>(gameRequest);
//Create signature
gameRequest = Utilities.CreateSignature(gameRequestDto, RequestType.Initiate);
//Set service
gameRequest.service = "RAZER";
//Add initiation request into database
_unitOfWork.GameRepository.Insert(gameRequest);
#region Call Razer initiate/confirm
//Call Razer for initiation
response = await Utilities.CallRazer(gameRequest, "purchaseinitiation");
//Read response
var htmlResponse = await response.Content.ReadAsStringAsync();
var gameResponse = JsonConvert.DeserializeObject<GameResponse>(htmlResponse);
//Adding initiation response into database
_unitOfWork.GameResponseRepository.Insert(gameResponse);
if (gameResponse.initiationResultCode == "00")
{
gameRequestDto = iMapper.Map<GameRequest, GameRequestDto>(gameRequest);
gameRequestDto.validatedToken = gameResponse.validatedToken;
//Create signature
var gameConfirmRequest = Utilities.CreateSignature(gameRequestDto, RequestType.Confirm);
//Transform DTO into GameRequest for calling Razer Initiate
var gameConfirmRequests = iMapper.Map<GameRequest, GameConfirmRequest>(gameConfirmRequest);
//Add confirm request into database
_unitOfWork.GameConfirmRequestRepository.Insert(gameConfirmRequests);
//Call Razer for confirm
response = await Utilities.CallRazer(gameConfirmRequest, "purchaseconfirmation");
//Read response
htmlResponse = await response.Content.ReadAsStringAsync();
var gameConfirmResponse = JsonConvert.DeserializeObject<GameConfirmResponse>(htmlResponse);
//Set service
gameConfirmResponse.service = "RAZER";
//Add confirm response into database
_unitOfWork.GameConfirmResponseRepository.Insert(gameConfirmResponse);
}
#endregion
await _unitOfWork.SaveAsync();
scope.Complete();
}
return response;
}
public void Dispose()
{
_unitOfWork.Dispose();
}
}
}
Generic Repo (shortened):
namespace DataModels
{
public class GenericRepository<TEntity> where TEntity : class
{
internal GameContext context;
internal DbSet<TEntity> dbSet;
public GenericRepository(GameContext context)
{
this.context = context;
this.dbSet = context.Set<TEntity>();
}
#region Public member methods...
public virtual async Task<List<TEntity>> GetGamesAsync(
Expression<Func<TEntity, bool>> where)
{
return await dbSet.Where(where).ToListAsync();
}
public virtual async Task<List<T>> GetGameBankProducts<T, TType>(Expression<Func<TEntity, bool>> where,
Expression<Func<TEntity, TType>> groupBy, Expression<Func<IGrouping<TType, TEntity>, T>> select)
{
return await dbSet.Where(where)
.GroupBy(groupBy)
.Select(select)
.ToListAsync();
}
#endregion
}
}
Unit of Work:
namespace DataModels
{
public class UnitOfWork : IDisposable
{
private readonly GameContext _context;
private readonly GenericRepository<GameRequest> gameRequestRepository;
private readonly GenericRepository<GameResponse> gameResponseRepository;
private readonly GenericRepository<GameConfirmRequest> gameConfirmRequestRepository;
private readonly GenericRepository<GameConfirmResponse> gameConfirmResponseRepository;
private readonly GenericRepository<GameBank> gameBankRepository;
private readonly GenericRepository<GameBankPin> gameBankPinRepository;
private readonly GenericRepository<ConfirmCancel> confirmCancelRepository;
private readonly GenericRepository<Recon> reconRepository;
private readonly GenericRepository<Company> userRepository;
public UnitOfWork(GameContext context)
{
_context = context;
}
public GenericRepository<GameRequest> GameRepository
{
get
{
return this.gameRequestRepository ?? new GenericRepository<GameRequest>(_context);
}
}
public GenericRepository<GameResponse> GameResponseRepository
{
get
{
return this.gameResponseRepository ?? new GenericRepository<GameResponse>(_context);
}
}
public GenericRepository<GameConfirmRequest> GameConfirmRequestRepository
{
get
{
return this.gameConfirmRequestRepository ?? new GenericRepository<GameConfirmRequest>(_context);
}
}
public GenericRepository<GameConfirmResponse> GameConfirmResponseRepository
{
get
{
return this.gameConfirmResponseRepository ?? new GenericRepository<GameConfirmResponse>(_context);
}
}
public GenericRepository<GameBank> GameBankRepository
{
get
{
return this.gameBankRepository ?? new GenericRepository<GameBank>(_context);
}
}
public GenericRepository<GameBankPin> GameBankPinRepository
{
get
{
return this.gameBankPinRepository ?? new GenericRepository<GameBankPin>(_context);
}
}
public GenericRepository<ConfirmCancel> ConfirmCancelRepository
{
get
{
return this.confirmCancelRepository ?? new GenericRepository<ConfirmCancel>(_context);
}
}
public GenericRepository<Recon> ReconRepository
{
get
{
return this.reconRepository ?? new GenericRepository<Recon>(_context);
}
}
public GenericRepository<Company> UserRepository
{
get
{
return this.userRepository ?? new GenericRepository<Company>(_context);
}
}
public void Save()
{
_context.SaveChanges();
}
public async Task SaveAsync()
{
await _context.SaveChangesAsync();
}
private bool disposed = false;
protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
_context.Dispose();
}
}
this.disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}
Unity config:
namespace Game
{
public static class UnityConfig
{
#region Unity Container
private static Lazy<IUnityContainer> container =
new Lazy<IUnityContainer>(() =>
{
var container = new UnityContainer();
RegisterTypes(container);
return container;
});
public static IUnityContainer Container => container.Value;
#endregion
public static void RegisterTypes(IUnityContainer container)
{
// Default empty Time manager!
container.RegisterType<IGameServices, GameServices>().RegisterType<UnitOfWork>(new TransientLifetimeManager());
container.RegisterType<IUserValidate, UserValidate>(new TransientLifetimeManager());
}
}
}
People keep telling me the best practice is to use "using" but how can I use using in the controller for the Dbcontext? I couldn't find a way.
Update1:
I have been told that instead of transaction scope, I should use dbcontext transaciton (using(var dbContextTransaction = context.Database.BeginTransaction())). I couldn't find a way to use dbcontext transaction in my Service class. I am injecting unit of work in Service class.
Update2:
Should I implement inside of unit of work and remove transactionscope inside of the Service?
UOW Save:
public void Save(){
try{
using (var transaction = _context.Database.BeginTransaction()){
try
{
_context.SaveChanges();
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
}catch (Exception e)
{//TODO:Logging}}
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 do requests to Github Api so I have async methods, these do this job. Before it, I always called they in method, that calls from command(actually DelegateCommand). But now I wanna do request in ViewModel because I need to display list on page. I am using Prism to wire view and viewmodel.
Because I can't make viewmodel async, I can't use await word, so I tried to do something like gets result from task, or task.wait. But with this I have the same result. My app stop works with white display when it did request. I read some info about that and I understood that call async method in not async method is bad, and it causes deadlock, but I don't know what to do with this. And I think deadlock causes that app stop works.
Here is method where app die:
public async Task<IEnumerable<RepositoryModel>> GetRepositoriesAsync()
{
try
{
var reposRequest = new RepositoryRequest { Sort = RepositorySort.FullName };
var gitHubRepos = await _gitHubClient.Repository.GetAllForCurrent(reposRequest); //async request, don't say about name convention, it is not my method.
var gitRemoteRepos = new List<RepositoryModel>();
foreach ( var repository in gitHubRepos )
{
var repos = new RepositoryModel();
repos.RepositoryTypeIcon = GetRepositoryTypeIcon(repository);
gitRemoteRepos.Add(repos);
}
return gitRemoteRepos;
}
catch ( WebException ex )
{
throw new Exception("Something wrong with internet connection, try to On Internet " + ex.Message);
}
catch ( Exception ex )
{
throw new Exception("Getting repos from github failed! " + ex.Message);
}
}
And here is viewmodel:
public class RepositoriesPageViewModel : BindableBase
{
private INavigationService _navigationService;
private readonly Session _session;
public ObservableCollection<RepositoryModel> Repositories { get; }
private readonly RepositoriesManager _repositoriesManager;
public RepositoriesPageViewModel(INavigationService navigationService, ISecuredDataProvider securedDataProvider)
{
_navigationService = navigationService;
var token = securedDataProvider.Retreive(ConstantsService.ProviderName, UserManager.GetLastUser());
_session = new Session(UserManager.GetLastUser(), token.Properties.First().Value);
var navigationParameters = new NavigationParameters { { "Session", _session } };
_repositoriesManager = new RepositoriesManager(_session);
var task = _repositoriesManager.GetRepositoriesAsync();
//task.Wait();
Repositories = task.Result as ObservableCollection<RepositoryModel>;
}
}
I recommend using my NotifyTask<T> type, which provides a data-bindable wrapper around Task<T>. I explain this pattern more completely in my article on async MVVM data binding.
public class RepositoriesPageViewModel : BindableBase
{
private INavigationService _navigationService;
private readonly Session _session;
public NotifyTask<ObservableCollection<RepositoryModel>> Repositories { get; }
private readonly RepositoriesManager _repositoriesManager;
public RepositoriesPageViewModel(INavigationService navigationService, ISecuredDataProvider securedDataProvider)
{
_navigationService = navigationService;
var token = securedDataProvider.Retreive(ConstantsService.ProviderName, UserManager.GetLastUser());
_session = new Session(UserManager.GetLastUser(), token.Properties.First().Value);
var navigationParameters = new NavigationParameters { { "Session", _session } };
_repositoriesManager = new RepositoriesManager(_session);
Repositories = NotifyTask.Create(GetRepositoriesAsync());
}
}
private async Task<ObservableCollection<RepositoryModel>> GetRepositoriesAsync()
{
return new ObservableCollection<RepositoryModel>(await _repositoriesManager.GetRepositoriesAsync());
}
Note that with this approach, your data binding would use Repositories.Result to access the actual collection. Other properties are also available, most notably Repositories.IsCompleted and Respositories.IsNotCompleted for showing/hiding busy spinners.