In a ASP NET Controller i have a service that returns a list of items.This service serves from the RAM the list to requesters.
The list can also be altered by a special group of users , so everytime it is altered i write the changes to disk and update my RAM from disk. (Reading my own writes this way)
From a JS client when i alter this list , the changes are written correctly on the disk , but when i forward a second request to get my list , i am served a stale list.I need to hit F5 for the client to get the right data.
I do not understand how does the RAM cache lags behind.
You can see in my service below that i have guarded the altering method with a lock.I have also tried without it to no avail.
Service
public class FileService : IADReadWrite {
private const int SIZE = 5;
private const string COMPUTER_FILE = #"computers.txt";
private List<Computer> computers = new List<Computer>();
private readonly object #filelock = new object();
private readonly Computer[] DEFAULT_COMPUTERS_LIST = new Computer[] {
new Computer(id:"W-CZC81371RS",Username:"A"),
new Computer(id:"W-CZC81371RQ",Username:"B"),
};
async Task<Computers> GetComputersAsymc() {
if (this.computers.Count == 0) {
var query = await Fetch();
this.computers = query.ToList();
}
var result = new Computers(this.computers);
return result;
}
public async Task<bool> AddComputerAsync(Computer computer) {
lock (filelock) {
if (this.computers.Any(x => x == computer)) {
return false;
}
this.computers.Add(computer);
File.WriteAllText(COMPUTER_FILE, JsonConvert.SerializeObject(this.computers, Formatting.Indented));
this.computers = JsonConvert.DeserializeObject<List<Computer>>(File.ReadAllText(COMPUTER_FILE));
}
return true;
}
---------------------Helpers --------------------------
private async Task<IEnumerable<Computer>> Fetch() {
if (!File.Exists(COMPUTER_FILE)) {
WriteComputersToDisk();
}
using (FileStream stream = new FileStream(COMPUTER_FILE, FileMode.Open, FileAccess.Read)) {
var raw = await File.ReadAllTextAsync(COMPUTER_FILE);
var comp = JsonConvert.DeserializeObject<List<Computer>>(raw);
return comp;
}
}
private void WriteComputersToDisk() {
var comps = DEFAULT_COMPUTERS_LIST;
var data = JsonConvert.SerializeObject(comps, Formatting.Indented);
File.WriteAllText(COMPUTER_FILE, data);
}
}
Controller
public class MyController:Controller
{
MyController(IADReadWrite service)
{
this.service=service;
}
IADReadWrite service;
[HttpGet]
public async Task<List<Computer>> GetAll()
{
return await service.GetComputersAsync();
}
[HttpPost]
public async Task AddComputer(Computer computer)
{
await service.AddComputerAsync(computer);
}
}
Scenario
Initial list : [0,1]
Client hits controller calling `AddComputer` {2}
I check the file , list is now: [0,1,2]
Client hits controller calling `GetComputers` -> it returns [0,1]
I hit F5 on the browser -> GetComputers gets hit again -> it returns [0,1,2]
P.S
I have not posted the Computer class since it does not matter in this scenario ( It implements IEquateable in case you are wondering if it is failing when i use the == operator.
The last 2 methods deal with the initialization of the Disk file.
Related
I recently started learning Unit Testing and now have the requirement write unit tests using Xunit and Moq for dot net core application.
I can write some very basic but when it comes to write them for complex classes , I am kind of stuck.
Below is the class I will be writing tests for.
public class AgeCategoryRequestHandler : IInventoryRequestHandler<InventoryRequest, HandlerResult>
{
private readonly IRepositoryResolver _repositoryResolver;
Hotels.HBSI.Logging.ILogger logger;
public AgeCategoryRequestHandler(IRepositoryResolver repositoryResolver, Hotels.HBSI.Logging.ILogger iLogger)
{
_repositoryResolver = repositoryResolver;
logger = iLogger;
}
public async Task<HandlerResult> Run(InventoryRequest inventoryRequest)
{
var result = await ProcessRequest(inventoryRequest);
return CreateResponse(inventoryRequest, result);
}
private async Task<int> ProcessRequest(InventoryRequest inventoryRequest)
{
logger.Info("AgeCategory requesthandler processrequest start");
var repository = _repositoryResolver.ResolveEstabAgeCategory();
if (repository is not null)
{
return await repository.InsertUpdateEstabAgeCategoryDetail(inventoryRequest.EstabAgeCategories)
.ConfigureAwait(false);
}
logger.Info("AgeCategory requesthandler processrequest complete");
return InernalError.reponotfound;
}
public HandlerResult CreateResponse(InventoryRequest inventoryRequest, int resultCount)
{
var requestCount = inventoryRequest.EstabAgeCategories.Count;
var handlerResult = new HandlerResult() { Id = RequestHandlerEnum.AgeCategrory.ToInt() };
if (requestCount > 0 && resultCount < requestCount)
{
handlerResult.IsSuccess = false;
handlerResult.ErrorCode = OTAErrorType.InvalidAgeCategory.ToInt();
}
else if (requestCount > 0 || requestCount == resultCount)
{
handlerResult.IsSuccess = true;
handlerResult.ErrorCode = 0;
}
return handlerResult;
}
}
Just to start , IRepositoryResolver and ILogger are in the constructor so I have created mock for these but unable to go beyond that as I am still in initial phase of learning.
Could someone explain me the steps/approach to accomplish this?.
Edit : What I have done so far is below ( can't figure out what are the things to be done and where to start or write )
Edit 2 : Did some more modifications to my test code , can someone comment if I am in right direction ? what else can I test ?
public class AgeCategoryRequestHandlerTest
{
private AgeCategoryRequestHandler _ageCategoryRequestHandler;
private readonly Mock<AgeCategoryRequestHandler> _ageCategory = new Mock<AgeCategoryRequestHandler>();
private readonly Mock<Hotels.HBSI.Logging.ILogger> _mockLogger = new Mock<Hotels.HBSI.Logging.ILogger>();
private readonly Mock<IRepositoryResolver> _mockRepositoryResolver = new Mock<IRepositoryResolver>();
public AgeCategoryRequestHandlerTest()
{
_ageCategoryRequestHandler = new AgeCategoryRequestHandler(_mockRepositoryResolver.Object, _mockLogger.Object);
}
[Fact]
public async void Testtt()
{
var fixture = new Fixture();
var inventory = fixture.Create<InventoryRequest>();
var hndlr = fixture.Create<HandlerResult>();
hndlr.ErrorCode = 0;
int resultCount = 3;
await _ageCategoryRequestHandler.Run(inventory);
HandlerResult response = _ageCategoryRequestHandler.CreateResponse(inventory, resultCount);
Assert.Equal(hndlr.ErrorCode, response.ErrorCode);
}
Tried running Chris B suggested code , was getting type conversion error EstabAgeCategories = new List<int>
Now I have used fixture for creating automatic objects and did some assert values. Below is the code sample
var fixture = new Fixture();
var inventoryRequest = fixture.Create<InventoryRequest>();
_mockRepository
.Setup(x => x.InsertUpdateEstabAgeCategoryDetail(inventoryRequest.EstabAgeCategories))
.ReturnsAsync(6);
_mockRepositoryResolver
.Setup(x => x.ResolveEstabAgeCategory())
.Returns(_mockRepository.Object);
// act
var result = await _ageCategoryRequestHandler.Run(inventoryRequest);
// assert
_mockRepository
.Verify(x => x.InsertUpdateEstabAgeCategoryDetail(inventoryRequest.EstabAgeCategories), Times.Once);
Assert.True(result.Id == 6);
Assert.True(result.ErrorCode == 0);
Assert.True(result.IsSuccess);
From the unit test code you've posted, it looks like you are getting confused on what to test.
Look at your class and identify your "public" interface i.e. what methods can be called from other parts of your code. You should really only test public methods. Private methods are usually tested via public methods.
Looking at AgeCategoryRequestHandler, you have two public methods - Run and CreateResponse. I would question whether CreateResponse needs to be public but we'll leave it for now. For each of these methods, you want to be asserting that the returned value is what you expect given the input value.
private AgeCategoryRequestHandler _ageCategoryRequestHandler;
// Not needed
private readonly Mock<AgeCategoryRequestHandler> _ageCategory = new Mock<AgeCategoryRequestHandler>();
private readonly Mock<Hotels.HBSI.Logging.ILogger> _mockLogger = new Mock<Hotels.HBSI.Logging.ILogger>();
private readonly Mock<IRepositoryResolver> _mockRepositoryResolver = new Mock<IRepositoryResolver>();
public AgeCategoryRequestHandlerTest()
{
_ageCategoryRequestHandler = new AgeCategoryRequestHandler(_mockRepositoryResolver.Object, _mockLogger.Object);
}
The set up of the unit test is going the right way - you have created mocks for your dependencies but I see you have created a mock for the class you are trying to test - this is not needed and can be removed. You want to be testing the actual class itself which you are initializing in the constructor.
public async Task<HandlerResult> Run(InventoryRequest inventoryRequest)
{
var result = await ProcessRequest(inventoryRequest);
return CreateResponse(inventoryRequest, result);
}
private async Task<int> ProcessRequest(InventoryRequest inventoryRequest)
{
_logger.LogInformation("AgeCategory requesthandler processrequest start");
var repository = _repositoryResolver.ResolveEstabAgeCategory();
if (repository != null)
{
return await repository.InsertUpdateEstabAgeCategoryDetail(inventoryRequest.EstabAgeCategories).ConfigureAwait(false);
}
_logger.LogInformation("AgeCategory requesthandler processrequest complete");
return 0;
}
We can test the public Run method by looking at the method and seeing what it is going to do when executed. Firstly, it's going to call a private method ProcessRequest. Inside ProcessRequest, the IRepositoryResolver dependency is going to be used. This means we need to "set up" this dependency in our unit test to satisfy the if (repository != null) condition.
I assume the IRepositoryResolver returns another interface (?) - something like:
public interface IRepository
{
Task<int> InsertUpdateEstabAgeCategoryDetail(List<int> x);
}
So in your unit test, you need to create a mock for the repository being returned from IRepositoryResolver:
private readonly Mock<IRepository> _mockRepository = new Mock<IRepository>();
Then, you need to set up the mock IRepositoryResolver to return the mock repository above:
_mockRepositoryResolver
.Setup(x => x.ResolveEstabAgeCategory())
.Returns(_mockRepository.Object);
This is to satisfy the if (repository != null) condition.
_mockRepository
.Setup(x => x.InsertUpdateEstabAgeCategoryDetail(inventoryRequest.EstabAgeCategories))
.ReturnsAsync(6);
Next, you need to set up the InsertUpdateEstabAgeCategoryDetail() method on the mock repository to return a value. This value is being returned by ProcessRequest() and then used to call CreateResponse(inventoryRequest, result) as the result parameter.
if (requestCount > 0 && resultCount < requestCount)
{
handlerResult.IsSuccess = false;
handlerResult.ErrorCode = (int)OTAErrorType.InvalidAgeCategory;
}
else if (requestCount > 0 || requestCount == resultCount)
{
handlerResult.IsSuccess = true;
handlerResult.ErrorCode = 0;
}
Now you can look at the CreateResponse method and by setting different values for inventoryRequest.EstabAgeCategories and setting up the mock _mockRepository.Setup(x => x.InsertUpdateEstabAgeCategoryDetail(inventoryRequest.EstabAgeCategories)).ReturnsAsync(6); to return different values, you can satisfy the different paths through the if statement.
CreateResponse is returning an instance of HandlerResult which in turn is being returned by Task<HandlerResult> Run. This is the returned object you want to make assertions on.
One of the unit test cases might look like this (I have not tested it myself):
[Fact]
public async Task GivenInventoryRequest_WhenRun_ThenHandlerResultReturned()
{
// arrange
var inventoryRequest = new InventoryRequest
{
EstabAgeCategories = new List<int>
{
1, 2, 3, 4, 5
}
};
_mockRepository
.Setup(x => x.InsertUpdateEstabAgeCategoryDetail(inventoryRequest.EstabAgeCategories))
.ReturnsAsync(6);
_mockRepositoryResolver
.Setup(x => x.ResolveEstabAgeCategory())
.Returns(_mockRepository.Object);
// act
var result = await _ageCategoryRequestHandler.Run(inventoryRequest);
// assert
_mockRepository
.Verify(x => x.InsertUpdateEstabAgeCategoryDetail(inventoryRequest.EstabAgeCategories), Times.Once);
Assert.True(result.Id == 0);
Assert.True(result.ErrorCode == 0);
Assert.False(result.IsSuccess);
}
I am trying to send a message. I have tried to connection id
public Task SendMessaageToConnectionID(string ConnectionID,string Message)
{
return Clients.Clients(ConnectionID).SendAsync("RecieveMessage", Message);
}
it is successfully done
Now I am trying this
public Task SendMessageToUser(string userId,string Message)
{
return Clients.Clients(userId).SendAsync(Message);
}
I am sending the user id of user Saved in AspNetUser Table
How Can I send this to a User ID or is there any other way except connection id to send the message to user?
SignalR won't store the UserId-ConnectionId mappings for us. We need to do that by our own. For example, when some user sets up a connection to the Hub, it should trigger a ReJoinGroup() method.
In addition, in order to make sure the Groups property works fine, you need also :
invoke RemoveFromGroupAsync to remove the old <connectionId, groupName> mapping
invoke AddToGroupAsync to add a new <connectionId, groupName> mapping.
Typically, you might want to store these information in Redis or RDBMS. For testing purpose, I create a demo that stores these mappings in memory for your reference:
public class MyHub:Hub
{
/// a in-memory store that stores the <userId, connectionId> mappings
private Dictionary<string, string> _userConn = new Dictionary<string,string>();
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
public override async Task OnConnectedAsync()
{
// get the real group Name by userId,
// for testing purpose, I use the userId as the groupName,
// in your scenario, you could use the ChatRoom Id
var groupName = Context.UserIdentifier;
await this.ReJoinGroup(groupName);
}
// whenever a connection is setup, invoke this Hub method to update the store
public async Task<KeyValuePair<string,string>> ReJoinGroup(string groupName)
{
var newConnectionId = Context.ConnectionId;
var userId = Context.UserIdentifier;
await this._semaphore.WaitAsync();
try{
if(_userConn.TryGetValue(userId, out var oldConnectionId))
{
_userConn[userId]= newConnectionId;
// remove the old connectionId from the Group
if(!string.IsNullOrEmpty(groupName)){
await Groups.RemoveFromGroupAsync(oldConnectionId, groupName);
await Groups.AddToGroupAsync(newConnectionId, groupName);
}
} else {
_userConn[userId]= newConnectionId;
if(!string.IsNullOrEmpty(groupName)){
await Groups.AddToGroupAsync(newConnectionId, groupName);
}
}
} finally{
this._semaphore.Release();
}
return new KeyValuePair<string,string>(userId, newConnectionId);
}
/// your SendMessageToUser() method
public async Task SendMessageToUser(string userId,string Message)
{
// get the connectionId of target user
var userConn = await this.GetUserConnection(userId);
if( userConn.Equals(default(KeyValuePair<string,string>))) {
throw new Exception($"unknown user connection with userId={userId}");
}
await Clients.Clients(userConn.Value).SendAsync(Message);
}
/// a private helper that returns a pair of <UserId,ConnectionId>
private async Task<KeyValuePair<string,string>> GetUserConnection(string userId)
{
KeyValuePair<string,string> kvp = default;
string newConnectionId = default;
await this._semaphore.WaitAsync();
try{
if(this._userConn.TryGetValue(userId, out newConnectionId)){
kvp= new KeyValuePair<string, string>(userId, newConnectionId);
}
} finally{
this._semaphore.Release();
}
return kvp;
}
}
Currently we are building a web application, desktop first, that needs device specific Razor Pages for specific pages. Those pages are really different from their Desktop version and it makes no sense to use responsiveness here.
We have tried to implement our own IViewLocationExpander and also tried to use the MvcDeviceDetector library (which is basically doing the same). Detection of the device type is no problem but for some reason the device specific page is not picked up and it is constantly falling back to the default Index.cshtml.
(edit: We're thinking about implementing something based on IPageConvention, IPageApplicationModelProvider or something ... ;-))
Index.mobile.cshtml
Index.cshtml
We have added the following code using the example of MvcDeviceDetector:
public static IMvcBuilder AddDeviceDetection(this IMvcBuilder builder)
{
builder.Services.AddDeviceSwitcher<UrlSwitcher>(
o => { },
d => {
d.Format = DeviceLocationExpanderFormat.Suffix;
d.MobileCode = "mobile";
d.TabletCode = "tablet";
}
);
return builder;
}
and are adding some route mapping
routes.MapDeviceSwitcher();
We expected to see Index.mobile.cshtml to be picked up when selecting a Phone Emulation in Chrome but that didnt happen.
edit Note:
we're using a combination of Razor Views/MVC (older sections) and Razor Pages (newer sections).
also not every page will have a mobile implementation. That's what would have a IViewLocationExpander solution so great.
edit 2
I think the solution would be the same as how you'd implement Culture specific Razor Pages (which is also unknown to us ;-)). Basic MVC supports Index.en-US.cshtml
Final Solution Below
If this is a Razor Pages application (as opposed to an MVC application) I don't think that the IViewLocationExpander interface is much use to you. As far as I know, it only works for partials, not routeable pages (i.e. those with an #page directive).
What you can do instead is to use Middleware to determine whether the request comes from a mobile device, and then change the file to be executed to one that ends with .mobile. Here's a very rough and ready implementation:
public class MobileDetectionMiddleware
{
private readonly RequestDelegate _next;
public async Task Invoke(HttpContext context)
{
if(context.Request.IsFromAMobileDevice())
{
context.Request.Path = $"{context.Request.Path}.mobile";
}
await _next.Invoke(context);
}
}
It's up to you how you want to implement the IsFromAMobileDevice method to determine the nature of the user agent. There's nothing stopping you using a third party library that can do the check reliably for you. Also, you will probably only want to change the path under certain conditions - such as where there is a device specific version of the requested page.
Register this in your Configure method early:
app.UseMiddleware<MobileDetectionMiddleware>();
I've finally found the way to do it convention based. I have implemented a IViewLocationExpander in order to tackle the device handling for basic Razor Views (including Layouts) and I've implemented IPageRouteModelConvention + IActionConstraint to handle devices for Razor Pages.
Note: this solution only seems to be working on ASP.NET Core 2.2 and up though. For some reason 2.1.x and below is clearing the constraints (tested with a breakpoint in a destructor) after they've been added (can probably be fixed).
Now I can have /Index.mobile.cshtml /Index.desktop.cshtml etc. in both MVC and Razor Pages.
Note: This solution can also be used to implement a language/culture specific Razor Pages (eg. /Index.en-US.cshtml /Index.nl-NL.cshtml)
public class PageDeviceConvention : IPageRouteModelConvention
{
private readonly IDeviceResolver _deviceResolver;
public PageDeviceConvention(IDeviceResolver deviceResolver)
{
_deviceResolver = deviceResolver;
}
public void Apply(PageRouteModel model)
{
var path = model.ViewEnginePath; // contains /Index.mobile
var lastSeparator = path.LastIndexOf('/');
var lastDot = path.LastIndexOf('.', path.Length - 1, path.Length - lastSeparator);
if (lastDot != -1)
{
var name = path.Substring(lastDot + 1);
if (Enum.TryParse<DeviceType>(name, true, out var deviceType))
{
var constraint = new DeviceConstraint(deviceType, _deviceResolver);
for (var i = model.Selectors.Count - 1; i >= 0; --i)
{
var selector = model.Selectors[i];
selector.ActionConstraints.Add(constraint);
var template = selector.AttributeRouteModel.Template;
var tplLastSeparator = template.LastIndexOf('/');
var tplLastDot = template.LastIndexOf('.', template.Length - 1, template.Length - Math.Max(tplLastSeparator, 0));
template = template.Substring(0, tplLastDot); // eg Index.mobile -> Index
selector.AttributeRouteModel.Template = template;
var fileName = template.Substring(tplLastSeparator + 1);
if ("Index".Equals(fileName, StringComparison.OrdinalIgnoreCase))
{
selector.AttributeRouteModel.SuppressLinkGeneration = true;
template = selector.AttributeRouteModel.Template.Substring(0, Math.Max(tplLastSeparator, 0));
model.Selectors.Add(new SelectorModel(selector) { AttributeRouteModel = { Template = template } });
}
}
}
}
}
protected class DeviceConstraint : IActionConstraint
{
private readonly DeviceType _deviceType;
private readonly IDeviceResolver _deviceResolver;
public DeviceConstraint(DeviceType deviceType, IDeviceResolver deviceResolver)
{
_deviceType = deviceType;
_deviceResolver = deviceResolver;
}
public int Order => 0;
public bool Accept(ActionConstraintContext context)
{
return _deviceResolver.GetDeviceType() == _deviceType;
}
}
}
public class DeviceViewLocationExpander : IViewLocationExpander
{
private readonly IDeviceResolver _deviceResolver;
private const string ValueKey = "DeviceType";
public DeviceViewLocationExpander(IDeviceResolver deviceResolver)
{
_deviceResolver = deviceResolver;
}
public void PopulateValues(ViewLocationExpanderContext context)
{
var deviceType = _deviceResolver.GetDeviceType();
if (deviceType != DeviceType.Other)
context.Values[ValueKey] = deviceType.ToString();
}
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
var deviceType = context.Values[ValueKey];
if (!string.IsNullOrEmpty(deviceType))
{
return ExpandHierarchy();
}
return viewLocations;
IEnumerable<string> ExpandHierarchy()
{
var replacement = $"{{0}}.{deviceType}";
foreach (var location in viewLocations)
{
if (location.Contains("{0}"))
yield return location.Replace("{0}", replacement);
yield return location;
}
}
}
}
public interface IDeviceResolver
{
DeviceType GetDeviceType();
}
public class DefaultDeviceResolver : IDeviceResolver
{
public DeviceType GetDeviceType() => DeviceType.Mobile;
}
public enum DeviceType
{
Other,
Mobile,
Tablet,
Normal
}
Startup
services.AddMvc(o => { })
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddRazorOptions(o =>
{
o.ViewLocationExpanders.Add(new DeviceViewLocationExpander(new DefaultDeviceResolver()));
})
.AddRazorPagesOptions(o =>
{
o.Conventions.Add(new PageDeviceConvention(new DefaultDeviceResolver()));
});
I am trying to refresh IMemoryCache programmatically. After researching a few links
about Eviction Calback and Clearing cache, I thought I could combine the strategies i.e. clear the cache which would cause the eviction callback to fire. However apparently the post eviction callback won't trigger when the cache is cleared using reflection because it seems the whole cache item with its options (that includes the callback ) is gone. (cache item count goes to 0)
So my question is about refreshing a cache item before expiration, as this issue is still open
private static Dictionary<string, CancellationTokenSource> tokenDict = new Dictionary<string, CancellationTokenSource>();
private MemoryCacheEntryOptions CacheOptions
{
get
{
var expirationToken = new CancellationChangeToken( new CancellationTokenSource(TimeSpan.FromMinutes(ExpirationMinutes + .01)).Token);
var options = new MemoryCacheEntryOptions()
// Do not remove due to memory pressure
.SetPriority(Microsoft.Extensions.Caching.Memory.CacheItemPriority.NeverRemove)
.SetSlidingExpiration(TimeSpan.FromMinutes(ExpirationMinutes))
// Force eviction to run AT expriry, default eviction happens when item is requested after expiry
.AddExpirationToken(expirationToken)
.RegisterPostEvictionCallback(callback: CacheItemRemoved, state: this);
tokenDict[cacheKey] = cancellationTokenSource;
return options;
}
}
private void CacheItemRemoved(object key, object value, EvictionReason reason, object state)
{
_logger.LogInformation($"Reloading {key} cache upon eviction");
switch (key)
{
case AccountCacheKey:
GetAccountCacheAsync();
break;
case FundCacheKey:
GetFundCacheAsync();
break;
default:
break;
}
}
private async Task<List<Account>> GetAccountCacheAsync()
{
return await _cache.GetOrCreateAsync(AccountCacheKey, async entry =>
{
entry.SetOptions(CacheOptions);
var accounts = await LoadAccountsAsync().ConfigureAwait(false);
return accounts;
}).ConfigureAwait(false);
}
private async Task<List<Fund>> GetFundCacheAsync()
{
return await _cache.GetOrCreateAsync(FundCacheKey, async entry =>
{
entry.SetOptions(CacheOptions);
var funds = await LoadFundsAsync().ConfigureAwait(false);
return funds;
}).ConfigureAwait(false);
}
public async Task RefreshCacheAsync()
{
var cacheKeys = new List<string> { AccountCacheKey, FundCacheKey };
foreach (var key in cacheKeys)
{
if (tokenDict.TryGetValue(key, out var token))
{
if (token != null && !token.IsCancellationRequested && token.Token.CanBeCanceled)
{
token.Cancel();
token.Dispose();
}
}
}
}
You already posted a link with the best approach, but you seem to have chosen to go with one of the lower rated answers, which actually doesn't work for your purposes. Instead, you should follow this answer. It creates a cache "manager" class that among other things employs CancellationTokenSource to handle the eviction. That's actually the same method that was recommended in the Github issue you linked, as well.
I am trying to get a proof of concept running with akka.net. I am sure that I am doing something terribly wrong, but I can't figure out what it is.
I want my actors to form a graph of nodes. Later, this will be a complex graph of business objekts, but for now I want to try a simple linear structure like this:
I want to ask a node for a neighbour that is 9 steps away. I am trying to implement this in a recursive manner. I ask node #9 for a neighbour that is 9 steps away, then I ask node #8 for a neighbour that is 8 steps away and so on. Finally, this should return node #0 as an answer.
Well, my code works, but it takes more than 4 seconds to execute. Why is that?
This is my full code listing:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Akka;
using Akka.Actor;
namespace AkkaTest
{
class Program
{
public static Stopwatch stopwatch = new Stopwatch();
static void Main(string[] args)
{
var system = ActorSystem.Create("MySystem");
IActorRef[] current = new IActorRef[0];
Console.WriteLine("Initializing actors...");
for (int i = 0; i < 10; i++)
{
var current1 = current;
var props = Props.Create<Obj>(() => new Obj(current1, Guid.NewGuid()));
var actorRef = system.ActorOf(props, i.ToString());
current = new[] { actorRef };
}
Console.WriteLine("actors initialized.");
FindNeighboursRequest r = new FindNeighboursRequest(9);
stopwatch.Start();
var response = current[0].Ask(r);
FindNeighboursResponse result = (FindNeighboursResponse)response.Result;
stopwatch.Stop();
foreach (var d in result.FoundNeighbours)
{
Console.WriteLine(d);
}
Console.WriteLine("Search took " + stopwatch.ElapsedMilliseconds + "ms.");
Console.ReadLine();
}
}
public class FindNeighboursRequest
{
public FindNeighboursRequest(int distance)
{
this.Distance = distance;
}
public int Distance { get; private set; }
}
public class FindNeighboursResponse
{
private IActorRef[] foundNeighbours;
public FindNeighboursResponse(IEnumerable<IActorRef> descendants)
{
this.foundNeighbours = descendants.ToArray();
}
public IActorRef[] FoundNeighbours
{
get { return this.foundNeighbours; }
}
}
public class Obj : ReceiveActor
{
private Guid objGuid;
readonly List<IActorRef> neighbours = new List<IActorRef>();
public Obj(IEnumerable<IActorRef> otherObjs, Guid objGuid)
{
this.neighbours.AddRange(otherObjs);
this.objGuid = objGuid;
Receive<FindNeighboursRequest>(r => handleFindNeighbourRequest(r));
}
public Obj()
{
}
private async void handleFindNeighbourRequest (FindNeighboursRequest r)
{
if (r.Distance == 0)
{
FindNeighboursResponse response = new FindNeighboursResponse(new IActorRef[] { Self });
Sender.Tell(response, Self);
return;
}
List<FindNeighboursResponse> responses = new List<FindNeighboursResponse>();
foreach (var actorRef in neighbours)
{
FindNeighboursRequest req = new FindNeighboursRequest(r.Distance - 1);
var response2 = actorRef.Ask(req);
responses.Add((FindNeighboursResponse)response2.Result);
}
FindNeighboursResponse response3 = new FindNeighboursResponse(responses.SelectMany(rx => rx.FoundNeighbours));
Sender.Tell(response3, Self);
}
}
}
The reason of such slow behavior is the way you use Ask (an that you use it, but I'll cover this later). In your example, you're asking each neighbor in a loop, and then immediately executing response2.Result which is actively blocking current actor (and thread it resides on). So you're essentially making synchronous flow with blocking.
The easiest thing to fix that, is to collect all tasks returned from Ask and use Task.WhenAll to collect them all, without waiting for each one in a loop. Taking this example:
public class Obj : ReceiveActor
{
private readonly IActorRef[] _neighbours;
private readonly Guid _id;
public Obj(IActorRef[] neighbours, Guid id)
{
_neighbours = neighbours;
_id = id;
Receive<FindNeighboursRequest>(async r =>
{
if (r.Distance == 0) Sender.Tell(new FindNeighboursResponse(new[] {Self}));
else
{
var request = new FindNeighboursRequest(r.Distance - 1);
var replies = _neighbours.Select(neighbour => neighbour.Ask<FindNeighboursResponse>(request));
var ready = await Task.WhenAll(replies);
var responses = ready.SelectMany(x => x.FoundNeighbours);
Sender.Tell(new FindNeighboursResponse(responses.ToArray()));
}
});
}
}
This one is much faster.
NOTE: In general you shouldn't use Ask inside of an actor:
Each ask is allocating a listener inside current actor, so in general using Ask is A LOT heavier than passing messages with Tell.
When sending messages through chain of actors, cost of ask is additionally transporting message twice (one for request and one for reply) through each actor. One of the popular patterns is that, when you are sending request from A⇒B⇒C⇒D and respond from D back to A, you can reply directly D⇒A, without need of passing the message through whole chain back. Usually combination of Forward/Tell works better.
In general don't use async version of Receive if it's not necessary - at the moment, it's slower for an actor when compared to sync version.