Can NServiceBus process events to different queues at different rates? - nservicebus

I have two subscribers to the same event. One subscriber writes to the database and the other caches data in memory. The former takes much longer than the latter and that's OK since the caching is more time critical than writing to the database. Some times the DB writer gets behind and its queue starts to grow, and that's OK (as long as eventually it catches up). But, it's unacceptable for the caching subscriber to get behind. It's OK for it to can get ahead of the DB writer when the DB writer cannot keep up. I want the two queues to be processed as fast as possible without the processing of one affecting the processing of the other.
But, what I see is that when the DB writer queue grows, the caching subscriber's queue grows. The two queues have the same number of pending items. They seem to be in lock step.
Profiling shows that the DB write takes about 500 times longer than the memory cache (which is not surprising). So, the caching subscriber could easily keep up, but it seems to be held back by the DB writer subscriber.
I'll apologize for code up front. It would be easier to understand if not for the endpoint wrapper code.
This method creates the endpoint as en event publisher. (My wrapper code provides different endpoint types for different purposes. I find the NSB endpoint abstraction to be lacking):
public async Task Start()
{
if (_endpoint == null)
_endpoint = await EventPublisherEndpoint.Start(EndpointName);
}
Here's the EventPublisherEndpoint:
public sealed class EventPublisherEndpoint : Endpoint, IEventPublisherEndpoint
{
private EventPublisherEndpoint(IEndpointInstance nsbEndpoint, string name) : base(nsbEndpoint, name) { }
/// <summary>
/// Create and start endpoint.
/// </summary>
public static async Task<IEventPublisherEndpoint> Start(string endpointName)
{
var ep = await ConfigureEndpoint(new EndpointConfig(), endpointName);
return new EventPublisherEndpoint(ep, endpointName);
}
public async Task Publish(object message)
{
await NsbEndpoint.Publish(message);
}
}
Here's the general endpoint factory:
protected static async Task<IEndpointInstance> ConfigureEndpoint(
IEndpointConfig config,
string endpointName,
Action<EndpointConfiguration> configureEndpoint = null,
Action<TransportExtensions<MsmqTransport>> configureTransport = null,
Action<PersistenceExtensions<SqlPersistence>> configurePersistence = null)
{
if (config == null)
throw new ArgumentNullException(nameof(config));
if (endpointName == null)
throw new ArgumentNullException(nameof(endpointName));
var fullName = GetFullName(endpointName);
try
{
IEndpointInstance ep = null;
await Logger.OperationAsync("Start endpoint: " + endpointName, async () =>
{
// eliminate existing error queue to clear it; is re-created below
string errorEndpointName = $"{fullName}.error";
EliminateQueue(errorEndpointName);
var endpointConfiguration = new EndpointConfiguration(fullName);
endpointConfiguration.UseSerialization<JsonSerializer>();
endpointConfiguration.SendFailedMessagesTo(errorEndpointName);
endpointConfiguration.EnableInstallers();
endpointConfiguration.DoNotCreateQueues(); // created explicitly (below)
endpointConfiguration.DefineCriticalErrorAction(OnCriticalError);
endpointConfiguration.LimitMessageProcessingConcurrencyTo(config.MaxConcurrency);
configureEndpoint?.Invoke(endpointConfiguration);
// NOTE:
// Using TransportTransactionMode.None disables retries.
// Using default (whatever that value is) results in runtime error on endpoint
// config that distributed transactions are not enabled (whatever that means).
// Stumbled into SendsAtomicWithReceive which seems to work; no error and
// retries happen.
const TransportTransactionMode transactionMode = TransportTransactionMode.SendsAtomicWithReceive;
var transport = endpointConfiguration
.UseTransport<MsmqTransport>()
.Transactions(transactionMode);
{
var routing = transport.Routing();
var instanceMappingFile = routing.InstanceMappingFile();
instanceMappingFile.FilePath(NsbInstanceMappingFile.DefaultFilePath);
}
configureTransport?.Invoke(transport);
var persistence = endpointConfiguration.UsePersistence<SqlPersistence>();
persistence.SqlVariant(SqlVariant.MsSqlServer);
persistence.ConnectionBuilder(() => new SqlConnection(config.PersistConnectionString));
persistence.TablePrefix(fullName + ".");
configurePersistence?.Invoke(persistence);
var subscriptions = persistence.SubscriptionSettings();
subscriptions.CacheFor(SubscriptionCachePeriod);
EnsureQueuesForEndpoint(fullName);
ep = await NServiceBus.Endpoint.Start(endpointConfiguration);
});
return ep;
}
catch (Exception exception)
{
throw new ApplicationException($"Unable to start endpoint '{endpointName}' for connection [{config.PersistConnectionString}].", exception);
}
}
Here's the publish code (inside a logging call):
public async Task Publish(DeviceOutput message)
{
await Logger.OperationWithoutBeginLogOrThrowAsync(
() => $"Publishing {message.GetType().Name} message: {message.ToString().Truncate()}.",
async () => { await _endpoint.Publish(message); });
}
Here's the event class:
public sealed class Datagram : DeviceOutput, IEvent
{
public Datagram(long deviceID, DatagramType payloadType, string payload) : base(deviceID)
{
Payload = payload;
PayloadType = payloadType;
}
public string Payload { get; set; }
public DatagramType PayloadType { get; set; }
public override string ToString()
{
return base.ToString() + Environment.NewLine + $"PayloadType:{PayloadType} Payload:{Payload.Truncate()}";
}
}
Here's one of the subscriber endpoint config:
public async Task Start()
{
if (_endpoint == null)
{
_endpoint = await EventSubscriberEndpoint<Datagram>.Start(
endpointName: "datagram-store-subscriber",
publisherEndpointName: DatagramPublisher.EndpointName);
}
}
And the associated handler:
public async Task Handle(Datagram datagram, IMessageHandlerContext context)
{
await _storer.Store(datagram);
}
Here's the other subscriber endpoint config:
public async Task Start()
{
if (_endpoint == null)
{
_endpoint = await EventSubscriberEndpoint<Datagram>.Start(
endpointName: "datagram-live-subscriber",
publisherEndpointName: DatagramPublisher.EndpointName);
}
}
And associated handler:
public async Task Handle(Datagram datagram, IMessageHandlerContext context)
{
//var start = DateTime.Now;
if (IsLiveDataEnabled)
{
await Logger.OperationWithoutBeginLogAsync(
() => $"Saving datagram: {datagram}",
async () => { await WriteData(datagram); });
}
//Logger.Info($"{DateTime.Now - start}");
}
Here's EventSubscriberEndpoint:
public sealed class EventSubscriberEndpoint<T> : Endpoint, IEventSubscriberEndpoint where T : IEvent
{
private EventSubscriberEndpoint(IEndpointInstance nsbEndpoint, string name) : base(nsbEndpoint, name) {}
public async Task Unsubscribe()
{
await NsbEndpoint.Unsubscribe(typeof(T), new UnsubscribeOptions());
}
/// <summary>
/// Create and start endpoint.
/// </summary>
public static async Task<IEventSubscriberEndpoint> Start(
string endpointName,
string publisherEndpointName,
Type[] excludeSubscriberTypes = null)
{
return await Start(endpointName, publisherEndpointName, typeof(T), excludeSubscriberTypes);
}
/// <summary>
/// Non-generic version of Start.
/// </summary>
private static async Task<IEventSubscriberEndpoint> Start(
string endpointName,
string publisherEndpointName,
Type messageType,
Type[] excludeSubscriberTypes = null)
{
return await Start(new EndpointConfig(), endpointName, publisherEndpointName, messageType, excludeSubscriberTypes);
}
private static async Task<IEventSubscriberEndpoint> Start(
IEndpointConfig config,
string endpointName,
string publisherEndpointName,
Type messageType,
Type[] excludeSubscriberTypes = null)
{
if (publisherEndpointName == null)
throw new ArgumentNullException(nameof(publisherEndpointName));
if (messageType == null)
throw new ArgumentNullException(nameof(messageType));
var ep = await ConfigureEndpoint(config,
endpointName,
endpointConfiguration =>
{
if (excludeSubscriberTypes != null)
endpointConfiguration.AssemblyScanner().ExcludeTypes(excludeSubscriberTypes);
}, transport =>
{
transport.Routing().RegisterPublisher(messageType, GetFullName(publisherEndpointName));
});
return new EventSubscriberEndpoint<T>(ep, endpointName);
}
}
Is there special config that allows queues to be processed independently of each other?
NServiceBus 6.2.1

Related

override default asp.net core cancelationtoken or change default timeout for requests

In my asp.net core 5.0 app, I call an async method that might take some time to be processed.
await someObject.LongRunningProcess(cancelationToken);
However, I want that method to be timeout after 5 seconds. I know that instead of "cancelationToken" passed by asp.net core action, I can use "CancellationTokenSource" :
var s_cts = new CancellationTokenSource();
s_cts.CancelAfter(TimeSpan.FromSeconds(5);
await someObject.LongRunningProcess(s_cts );
Is it possible to use "CancellationTokenSource" as a default "Cancelation Token" policy for all asp.net core requests ? I mean override the one which is passed as a parameter of the action ?
Or is it possible to change the default timeout for all request in asp.net core 5.0 ?
[Update]
Customizing the CancellationToken passed to actions
You need to replace the default CancellationTokenModelBinderProvider that binds HttpContext.RequestAborted token to CancellationToken parameters of actions.
This involves creating a custom IModelBinderProvider. Then we can replace the default binding result with our own.
public class TimeoutCancellationTokenModelBinderProvider : IModelBinderProvider
{
public IModelBinder? GetBinder(ModelBinderProviderContext context)
{
if (context?.Metadata.ModelType != typeof(CancellationToken))
{
return null;
}
var config = context.Services.GetRequiredService<IOptions<TimeoutOptions>>().Value;
return new TimeoutCancellationTokenModelBinder(config);
}
private class TimeoutCancellationTokenModelBinder : CancellationTokenModelBinder, IModelBinder
{
private readonly TimeoutOptions _options;
public TimeoutCancellationTokenModelBinder(TimeoutOptions options)
{
_options = options;
}
public new async Task BindModelAsync(ModelBindingContext bindingContext)
{
await base.BindModelAsync(bindingContext);
if (bindingContext.Result.Model is CancellationToken cancellationToken)
{
// combine the default token with a timeout
var timeoutCts = new CancellationTokenSource();
timeoutCts.CancelAfter(_options.Timeout);
var combinedCts =
CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken);
// We need to force boxing now, so we can insert the same reference to the boxed CancellationToken
// in both the ValidationState and ModelBindingResult.
//
// DO NOT simplify this code by removing the cast.
var model = (object)combinedCts.Token;
bindingContext.ValidationState.Clear();
bindingContext.ValidationState.Add(model, new ValidationStateEntry() { SuppressValidation = true });
bindingContext.Result = ModelBindingResult.Success(model);
}
}
}
}
class TimeoutOptions
{
public int TimeoutSeconds { get; set; } = 30; // seconds
public TimeSpan Timeout => TimeSpan.FromSeconds(TimeoutSeconds);
}
Then add this provider to Mvc's default binder provider list. It needs to run before all others, so we insert it at the beginning.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.Configure<MvcOptions>(options =>
{
options.ModelBinderProviders.RemoveType<CancellationTokenModelBinderProvider>();
options.ModelBinderProviders.Insert(0, new TimeoutCancellationTokenModelBinderProvider());
});
// remember to set the default timeout
services.Configure<TimeoutOptions>(configuration => { configuration.TimeoutSeconds = 2; });
}
Now ASP.NET Core will run your binder whenever it sees a parameter of CancellationToken type, which combines HttpContext.RequestAborted token with our timeout token. The combined token is triggered whenever one of its component is cancelled (due to timeout or request abortion, whichever is cancelled first)
[HttpGet("")]
public async Task<IActionResult> Index(CancellationToken cancellationToken)
{
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); // throws TaskCanceledException after 2 seconds
return Ok("hey");
}
References:
https://github.com/dotnet/aspnetcore/blob/348b810d286fd2258aa763d6eda667a83ff972dc/src/Mvc/Mvc.Core/src/ModelBinding/Binders/CancellationTokenModelBinder.cs
https://abdus.dev/posts/aspnetcore-model-binding-json-query-params/
https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding?view=aspnetcore-5.0#custom-model-binder-sample
One approach to solve this problem is wrapping that logic inside a class. Write a class that runs a task with a configurable timeout.
Then register it in DI, then use it anywhere you want to reuse the configuration.
public class TimeoutRunner
{
private TimeoutRunnerOptions _options;
public TimeoutRunner(IOptions<TimeoutRunnerOptions> options)
{
_options = options.Value;
}
public async Task<T> RunAsync<T>(Func<CancellationToken, Task<T>> runnable,
CancellationToken cancellationToken = default)
{
// cancel the task as soon as one of the tokens is set
var timeoutCts = new CancellationTokenSource();
var token = timeoutCts.Token;
if (cancellationToken != default)
{
timeoutCts.CancelAfter(_options.Timeout);
var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken);
token = combinedCts.Token;
}
return await runnable(token);
}
}
internal static class ServiceCollectionExtensions
{
public static IServiceCollection AddTimeoutRunner(this IServiceCollection services,
Action<TimeoutRunnerOptions> configure = null)
{
if (configure != null)
{
services.Configure<TimeoutRunnerOptions>(configure);
}
return services.AddTransient<TimeoutRunner>();
}
}
public class TimeoutRunnerOptions
{
public int TimeoutSeconds { get; set; } = 10;
public TimeSpan Timeout => TimeSpan.FromSeconds(TimeoutSeconds);
}
you'd then register this in Startup class,
public void ConfigureServices(IServiceCollection services)
{
services.AddTimeoutRunner(options =>
{
options.TimeoutSeconds = 10;
});
}
then consume it wherever you need that global option:
public class MyController : ControllerBase
{
private TimeoutRunner _timeoutRunner;
public MyController(TimeoutRunner timeoutRunner)
{
_timeoutRunner = timeoutRunner;
}
public async Task<IActionResult> DoSomething(CancellationToken cancellationToken)
{
await _timeoutRunner.RunAsync(
async (CancellationToken token) => {
await Task.Delay(TimeSpan.FromSeconds(20), token);
},
cancellationToken
);
return Ok();
}
}
Running a task before every action dispatch
Method 1: Action filters
We can use action filters to run a task before/after every request.
public class ApiCallWithTimeeotActionFilter : IAsyncActionFilter
{
private TimeoutRunner _runner;
public ApiCallWithTimeeotActionFilter(TimeoutRunner runner)
{
_runner = runner;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var result = await _runner.RunAsync(
async (CancellationToken token) =>
{
await Task.Delay(TimeSpan.FromSeconds(20), token);
return 42;
},
default
);
await next();
}
}
then to use it annotate a class with [TypeFilter(typeof(MyAction))]:
[TypeFilter(typeof(ApiCallWithTimeeotActionFilter))]
public class MyController : ControllerBase { /* ... */ }
Method 2: Middlewares
Another option is to use a middleware
class ApiCallTimeoutMiddleware
{
private TimeoutRunner _runner;
public ApiCallTimeoutMiddleware(TimeoutRunner runner)
{
_runner = runner;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
// run a task before every request
var result = await _runner.RunAsync(
async (CancellationToken token) =>
{
await Task.Delay(TimeSpan.FromSeconds(20), token);
return 42;
},
default
);
await next(context);
}
}
then attach the middleware in Startup.Configure method:
public void Configure(IApplicationBuilder app)
{
app.UseMiddleware<ApiCallTimeoutMiddleware>();
app.UseRouting();
app.UseEndpoints(e => e.MapControllers());
}

cache TryGetValue versus Get

This is how I am implementing my CacheManager. The problem I am facing is that TryGetValue will always return null in RemoveFromCache function. This function is called after one of the tokens has expired and so I am trying to clear that token from the List in cache, while GetAllTokens is returning full list of all tokens. AddTokenToCache is working correctly.
Its a WebAPI on ASPNET-Core 3.0
CacheManager.cs
public class CacheManager : ICacheManager
{
private IMemoryCache _cache;
public CacheManager(IMemoryCache cache) {
_cache = cache;
}
public void AddTokenToCache(string appName, string tokenString)
{
List<Token> tokens = new List<Token>();
//save this token against the application record in-memory
if (!_cache.TryGetValue(CacheHelper.CacheKey_Tokens, out tokens))
{
if (tokens == null)
tokens = new List<Token>();
}
tokens.Add(new Token
{
AppName = appName,
GeneratedAt = DateTime.Now,
TokenId = tokenString
});
// Set cache options.
var cacheEntryOptions = new MemoryCacheEntryOptions()
;// .SetSlidingExpiration(TimeSpan.FromSeconds(180)); //3 minutes
_cache.Set(CacheHelper.CacheKey_Tokens, tokens, cacheEntryOptions);
}
public List<Token> GetAllTokens()
{
return _cache.Get<List<Token>>(CacheHelper.CacheKey_Tokens);
}
public bool RemoveFromCache(string tokenId)
{
List<Token> tokens = new List<Token>();
//remove this token from memory
if (!_cache.TryGetValue(CacheHelper.CacheKey_Tokens, out tokens)) {
return false;
}
else
{
if (tokens != null && tokens.Count > 0)
{
//_logger.LogInfo("Processing token");
//trimming quotations from the string
tokenId = tokenId.Substring(1, tokenId.Length - 2);
int index = tokens.FindIndex(t => t.TokenId == tokenId);
if (index >= 0)
tokens.RemoveAt(index);
var cacheEntryOptions = new MemoryCacheEntryOptions();
_cache.Set(CacheHelper.CacheKey_Tokens, tokens, cacheEntryOptions);
return true;
}
}
return false;
}
}
My calling sequence is:
AddTokenToCache (token is added successfully to cache)
GetAllToken (shows a token is added to cache)
AddTokenToCache (token is added successfully to cache)
GetAllToken (shows both tokens are added to cache)
Fire TokenExpired event which calls RemoveFromCache (tokens is null)
GetAllToken (shows both tokens are added to cache)
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<ILoggerManager, LoggerManager>();
services.AddMemoryCache();
services.AddDbContext<GEContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddControllers();
services.AddRazorPages();
services.AddSingleton<ICacheManager, CacheManager>();
RegisterHandlerforTokenExpiredEvent(services);
//other code removed for brevity
}
public void RegisterHandlerforTokenExpiredEvent(IServiceCollection services)
{
var sp = services.BuildServiceProvider();
var jwtManager = sp.GetService<IJWTAuthenticationManager>(); //publisher
var cacheManager = sp.GetService<ICacheManager>(); //subscriber
jwtManager.TokenExpired += cacheManager.OnTokenExpired;
}
That's because you built another ServiceProvider by services.BuildServiceProvider():
public void RegisterHandlerforTokenExpiredEvent(IServiceCollection services)
{
var sp = services.BuildServiceProvider(); // this is a different service provider from the default one built by ASP.NET Core itself.
var jwtManager = sp.GetService<IJWTAuthenticationManager>(); //publisher
var cacheManager = sp.GetService<ICacheManager>(); //subscriber
// it doesn't work because the cacheManager is not the same instance that you use in the controllers
jwtManager.TokenExpired += cacheManager.OnTokenExpired;
}
As a result, the ICacheManager instance you get is NOT the same singleton that you inject in Controllers/Other Services. In other words, you'll have two different ICacheManager instance !
As a golden rule, DO NOT build another copy of ServiceProvider by services.BuildServiceProvider() in you application layer code unless you're pretty sure it's fine for you.
How to fix
Instead of building another copy of service provider and then getting another instance, you should always use IoC instead of Service Locator Pattern.
Seems that your JWTAuthenticationManager is a singleton and you want to bind the Event handler at startup-time. If that's the case, you could register an HostedService.
public class MyHostedService : IHostedService
{
private readonly IJWTAuthenticationManager _jWTAuthManager;
private readonly ICacheManager _cacheManager;
// suppose your IJWTAuthenticationManager is a singleton service
public MyHostedService(IJWTAuthenticationManager jWTAuthManager, ICacheManager cacheManager)
{
this._jWTAuthManager = jWTAuthManager;
this._cacheManager = cacheManager;
}
public Task StartAsync(CancellationToken cancellationToken)
{
this._jWTAuthManager.TokenExpired += this._cacheManager.OnTokenExpired;
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
this._jWTAuthManager.TokenExpired -= this._cacheManager.OnTokenExpired;
return Task.CompletedTask;
}
}
and register this service within Startup:
services.AddHostedService<MyHostedService>();
Another way that doesn't need HostedService and starts at start-up time:
Get the service and bind the event before Host.Run():
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
var jwtMgr = host.Services.GetRequiredService<IJWTAuthenticationManager>();
var cacheMgr = host.Services.GetRequiredService<ICacheManager>();
jwtMgr.TokenExpired = cacheMgr.OnTokenExpired;
host.Run();
}

How do I push data from hub to client every second using SignalR

Working on building signalR hub, I'm able to get data from hub to the client but I'm, not sure how do I push it every 1 second.
I'm not sure where do I set the timer in the controller where getApps method exists or in the hub?
Hub:
public class nphub : Hub
{
public readonly sbController _sbcontroller;
public nphub(sbController sbcontroller)
{
_sbcontroller = sbcontroller;
}
public async Task NotifyConnection()
{
IActionResult result = await _sbcontroller.getApps();
await Clients.All.SendAsync("TestBrodcasting", result);
}
}
In Controller:
public async Task<IActionResult> getApps()
{
// var request = new HttpRequestMessage(HttpMethod.Get, "apps");
// var response = await _client_NP.SendAsync(request);
// var json = await response.Content.ReadAsStringAsync();
return Ok($"Testing a Basic HUB at {DateTime.Now.ToLocalTime()}");
}
Client:
let connection = new signalR.HubConnectionBuilder()
.withUrl("/nphub").build();
connection.start().then(function () {
TestConnection();
}).catch(function (err) {
return console.error(err.toString());
});
function TestConnection() {
connection.invoke("NotifyConnection").catch(function (err) {
return console.error(err.toString());
});
}
connection.on("TestBrodcasting", function (time) {
document.getElementById('broadcastDiv').innerHTML = time.value;
document.getElementById('broadcastDiv').style.display = "block";
});
Just for the test purpose to see realtime changes, I'm trying to return time. I'm able to see time on the client but it's not changing.
You need to use a hosted service, as described in the docs. Add a class like:
internal class SignalRTimedHostedService : IHostedService, IDisposable
{
private readonly IHubContext<nphub> _hub;
private readonly ILogger _logger;
private Timer _timer;
public SignalRTimedHostedService(IHubContext<nphub> hub, ILogger<SignalRTimedHostedService> logger)
{
_hub = hub;
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Background Service is starting.");
_timer = new Timer(DoWork, null, TimeSpan.Zero,
TimeSpan.FromSeconds(1));
return Task.CompletedTask;
}
private void DoWork(object state)
{
_logger.LogInformation("Timed Background Service is working.");
// send message using _hub
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Background Service is stopping.");
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
Note: A hosted service lives in singleton scope. You can inject IHubContext<T> directly, though, because it too is in singleton scope.
Then in ConfigureServices:
services.AddHostedService<SignalRTimedHostedService>();

How are threads reused in Asp Net Core

Hello i am building an tcp server where i will have multiple clients connected that will send and receive data to the server.
I want to know if the framework does not create a 1:1 Thread to Client ratio but uses the threadpool how do the following happen:
1.If the method that gets executed after accepting the socket contains a loop inside it, won't the allocated thread (by the threadpool) get blocked on a client context?
2.Where is the context for each client stored?
P.S In my picture i do not understand how the blue thread (given by the threadpool to service the two clients)gets reused.
The code below contains the Handler (contains all connections) and the Client (a socket wrapper , with basic read/write functionality).
Sockets Handler
class Handler
{
private Dictionary<string, Client> clients = new Dictionary<string, Client>();
private object Lock = new object();
public Handler()
{
}
public async Task LoopAsync(WebSocketManager manager)
{
WebSocket clientSocket = await manager.AcceptWebSocketAsync();
string clientID = Ext.MakeId();
using(Client newClient = Client.Create(clientSocket, clientID))
{
while (newClient.KeepAlive)
{
await newClient.ReceiveFromSocketAsync();
}
}
}
public bool RemoveClient(string ID)
{
bool removed = false;
lock (Lock)
{
if (this.clients.TryGetValue(ID, out Client client))
{
removed= this.clients.Remove(ID);
}
}
return removed;
}
}
SocketWrapper
class Client:IDisposable
{
public static Client Create(WebSocket socket,string id)
{
return new Client(socket, id);
}
private readonly string ID;
private const int BUFFER_SIZE = 100;
private readonly byte[] Buffer;
public bool KeepAlive { get; private set; }
private readonly WebSocket socket;
private Client(WebSocket socket,string ID)
{
this.socket = socket;
this.ID = ID;
this.Buffer = new byte[BUFFER_SIZE];
}
public async Task<ReadOnlyMemory<byte>> ReceiveFromSocketAsync()
{
WebSocketReceiveResult result = await this.socket.ReceiveAsync(this.Buffer, CancellationToken.None);
this.KeepAlive = result.MessageType==WebSocketMessageType.Close?false:true;
return this.Buffer.AsMemory();
}
public async Task SendToSocketAsync(string message)
{
ReadOnlyMemory<byte> memory = Encoding.UTF8.GetBytes(message);
await this.socket.SendAsync(memory, WebSocketMessageType.Binary,true,CancellationToken.None);
}
public void Dispose()
{
this.socket.Dispose();
}
}
The service that will get injected in the application:
class SocketService
{
Handler hander;
public SocketService(Handler _handler)
{
this.hander = _handler;
}
RequestDelegate next;
public async Task Invoke(HttpContext context)
{
if (!context.WebSockets.IsWebSocketRequest)
{
await this.next(context);
return;
}
await this.hander.AddClientAsync(context.WebSockets);
}
}

ASP .NET Entity Framework Core Cannot access a disposed object

Wanting to get into .NET Core, I created a WEB API that takes a file upload and then saves the transactions in the file into a DB table. I'm using .NET Core 2 with Entity Framework Core. I created my context using the example from here.
My problem is that I get the error "System.ObjectDisposedException Cannot access a disposed object" when it tries to save to the context object in my repository. It's a simple stack, so I'm hoping someone can help me out.
My container setup looks like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddDbContext<SyncFinContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddScoped<ITransactionProcessor, TransactionProcessor>();
services.AddScoped<ITransactionRepository, TransactionRepository>();
}
My DBInitializer which I also got from the link above:
public static class DbInitializer
{
public static async Task Initialize(SyncFinContext context)
{
await context.Database.EnsureCreatedAsync();
// Look for any students.
if (context.Transactions.Any())
{
return; // DB has been seeded
}
var ts = new Transaction[]
{
// insert test data here
};
await context.SaveChangesAsync();
}
}
My DB Context:
public class SyncFinContext : DbContext
{
public SyncFinContext(DbContextOptions<SyncFinContext> options) : base(options)
{
}
public DbSet<Transaction> Transactions { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Transaction>().ToTable("Transactions");
}
}
My Controller looks like this:
[Produces("application/json")]
public class TransactionController : Controller
{
ITransactionRepository _transactionRepository { get; set; }
ITransactionProcessor _transactionProcessor { get; set; }
public TransactionController(ITransactionRepository m, ITransactionProcessor p) : base()
{
_transactionRepository = m;
_transactionProcessor = p;
}
// POST: transaction/import
[HttpPost]
public async void Import(List<IFormFile> files)
{
if (files == null || files.Count == 0)
{
throw new FileNotFoundException("No file was received.");
}
// copy file to temp location so that it can be processed
var filepath = Path.GetTempFileName();
using (var stream = new FileStream(filepath, FileMode.Create))
{
await files[0].CopyToAsync(stream);
}
ImportTransactionRequest input = new ImportTransactionRequest
{
FilePath = filepath
};
var transactions = await _transactionProcessor.ReadDocument(filepath);
await _transactionRepository.AddBulk(transactions);
}
}
And my repository looks like this:
public class TransactionRepository : ITransactionRepository
{
// TODO: move the context
private SyncFinContext _context;
public TransactionRepository(SyncFinContext context)
{
_context = context;
}
public async Task AddBulk(List<Transaction> transactions)
{
foreach(var t in transactions)
{
await _context.Transactions.AddAsync(t);
}
_context.SaveChanges();
}
}
For full transparency, the transaction Processor just gets a list of rows from a csv:
public async Task<List<Transaction>> ReadDocument(string filepath)
{
try
{
var ret = new List<Transaction>();
var lines = await File.ReadAllLinesAsync(filepath);
foreach (var line in lines)
{
var parts = line.Split(',');
var tx = new Transaction
{
PostedDate = DateTime.Parse(parts[0]),
TransactionDate = DateTime.Parse(parts[1]),
Description = parts[2],
Deposit = ParseDecimal(parts[3]),
Withdrawal = ParseDecimal(parts[4]),
Balance = ParseDecimal(parts[5])
};
ret.Add(tx);
}
return ret;
}
catch(Exception e)
{
throw;
}
}
I've read where the whole stack must be async in order for the db context instance to be available, and, unless I'm doing it wrong, I seem to be doing that, as you can see above.
My expectations are that AddDbContext() will indeed properly scope the context to be available throughout the stack unless I explicitly dispose of it. I have not found anything to make me think otherwise.
I've tried hard-coding data in my DB Initializer also, as I read that may be a factor, but that does not solve the problem. Not sure what else to try. If someone can give me some ideas I would appreciate it.
The Import() action method needs to have a return type of Task. MVC will await execute an action method with a return type of Task.
Also, probably best to get in the habit of returning an IActionResult on your action methods. The task based equivalent is Task<IActionResult>. This makes your controllers easier to test.
Since the AddBulk(List<Transaction> transactions) method is public async Task, the DbContext will be disposed if any part returns void (not awaited) at any point.
Try changing _context.SaveChanges();
To await _context.SaveChangesAsync();
This would ensure a Task is being returned and not void.
https://stackoverflow.com/a/46308661/3062956
https://learn.microsoft.com/en-us/ef/core/saving/async