Getting result from async call in a foreach loop - c#

I am trying to get a list of offers that are fetched from an external service.
public void TriggerPurchase()
{
List<string> offers = listener.GetOffers();
Debug.Log("OFFERS " + offers[0]);
}
Here is the fetch function
public List<string> GetOffers()
{
var purchases = GetComponent<Purchases>();
purchases.GetOfferings((offerings, error) =>
{
if (offerings.Current != null && offerings.Current.AvailablePackages.Count != 0)
{
List<string> offers = new List<string>();
foreach (var package in offerings.Current.AvailablePackages)
{
//Debug.Log("Package " + package);
if (package == null) continue;
offers.Add(package.StoreProduct.PriceString);
}
return offers; // Anonymouse function converted to a void returning delegate cannot return a value
}
});
}
I get the Anonymouse function converted to a void returning
I would like to get a list with offers?
**UPDATE
Adding the implementation of GetOfferings
/// <summary>
/// Callback for <see cref="Purchases.GetOfferings"/>.
/// </summary>
/// <param name="offerings"> The <see cref="Offerings"/> object if the request was successful, null otherwise.</param>
/// <param name="error"> The error if the request was unsuccessful, null otherwise.</param>
public delegate void GetOfferingsFunc(Offerings offerings, Error error);
private GetOfferingsFunc GetOfferingsCallback { get; set; }
///
/// <param name="callback"> A completion block called when offerings are available.
/// Called immediately if offerings are cached. <see cref="Offerings"/> will be null if an error occurred.
/// </param>
///
/// <seealso href="https://docs.revenuecat.com/docs/displaying-products"/>
///
public void GetOfferings(GetOfferingsFunc callback)
{
GetOfferingsCallback = callback;
_wrapper.GetOfferings();
}

void methods don't return values. If you want your method to return a result you must change the type of the method and delegate:
public delegate Task<List<string>> GetOfferingsFuncAsync(Offerings offerings, Error error);
private GetOfferingsFunc GetOfferingsCallback { get; set; }
public async Task<List<string>> GetOfferingsAsync(GetOfferingsFunc callback)
{
GetOfferingsCallback = callback;
_wrapper.GetOfferings();
return await this.GetOfferingsCallback.Invoke(arg1, arg2);
}
Now you can handle the expected result properly:
public async Task<List<string>> GetOffers()
{
var purchases = GetComponent<Purchases>();
// Return the result.
return await purchases.GetOfferingsAsync(async (offerings, error) =>
{
if (offerings.Current != null && offerings.Current.AvailablePackages.Count != 0)
{
List<string> offers = new List<string>();
foreach (var package in offerings.Current.AvailablePackages)
{
//Debug.Log("Package " + package);
if (package == null) continue;
await SomeAsyncApiCall();
offers.Add(package.StoreProduct.PriceString);
}
// Because the delegate is no longer 'void'
// you can now return a result
return offers;
}
});
}

Related

ServiceStack OnDeserialized Equivalent

I am deserialize a websocket message in real time. In the message (string of json) I receive there is a unix timestamp (long). As soon as each object is deserialized I need it to call a method ASAP so that I can capture the delay between the time the message was sent and received. With Json.NET that was simple I just added this method to my DataContract class:
[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
Timestamp = DateTimeOffset.FromUnixTimeMilliseconds(TimestampLong).LocalDateTime;
Delay = DateTime.Now - Timestamp;
}
I would much prefer to use ServiceStack's deserializer going forward for various reasons but I can't seem to figure out a way to do this. I did find this StackOverflow post from almost 10 years ago but I'm hoping that's changed or that there is a workaround that I can use.
ServiceStack.Text doesn't support these attributes by default, but you can implement serialization callbacks with Custom Type Configuration, e.g:
JsConfig<MyType>.OnDeserializedFn = o =>
{
o.OnDeserialized(null);
return o;
};
The SerializationHookTests.cs shows how you can use the Type Filters to wire up these callbacks for a type using this helper:
static void AddSerializeHooksForType<T>()
{
Type type = typeof(T);
System.Reflection.MethodInfo[] typeMethods = type
.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
var onSerializingMethods = typeMethods.Where(m =>
m.GetCustomAttributes(typeof(OnSerializingAttribute), true).Length > 0);
var OnDeserializedMethods = typeMethods.Where(m =>
m.GetCustomAttributes(typeof(OnDeserializedAttribute), true).Length > 0);
var OnSerializedMethods = typeMethods.Where(m =>
m.GetCustomAttributes(typeof(OnSerializedAttribute), true).Length > 0);
Object[] Parameters = { null };
if (onSerializingMethods.Any()) {
ServiceStack.Text.JsConfig<T>.OnSerializingFn = s => {
foreach (var method in onSerializingMethods)
method.Invoke(s, Parameters);
return s;
};
}
if (OnSerializedMethods.Any()) {
ServiceStack.Text.JsConfig<T>.OnSerializedFn = s => {
foreach (var method in OnSerializedMethods)
method.Invoke(s, Parameters);
};
}
if (OnDeserializedMethods.Any()) {
ServiceStack.Text.JsConfig<T>.OnDeserializedFn = s => {
foreach (var method in OnDeserializedMethods)
method.Invoke(s, Parameters);
return s;
};
}
}
Which you can wire up for a type with:
AddSerializeHooksForType<HookTest>();
Where it will call the desired callbacks, e.g:
public class HookTest
{
/// <summary>
/// Will be executed when deserializing starts
/// </summary>
[OnDeserializing]
protected void OnDeserializing(StreamingContext ctx) { }
/// <summary>
/// Will be executed when deserializing finished
/// </summary>
[OnDeserialized]
protected void OnDeserialized(StreamingContext ctx) { }
/// <summary>
/// Will be executed when serializing starts
/// </summary>
[OnSerializing]
protected void OnSerializing(StreamingContext ctx) { }
/// <summary>
/// Will be executed when serializing finished
/// </summary>
[OnSerialized]
protected void OnSerialized(StreamingContext ctx) { }
}

Why is AutoResetEvent.waitone returning false in my test?

Hello so I'm doing a unit test in c# with Mono. I'm testing a async method that uses delegates to return the response of a web service.
So here is my test:
[Test]
public void TestCreateUser()
{
Debug.Log("TestCreateUser");
Vizzario.Instance.OnCreateUserSuccess += delegate(CreateUserResponse x)
{
Debug.Log("TestCreateUser: " + x.Response);
UnityEngine.Assertions.Assert.IsNotNull(x.TokenResponse.AuthToken);
UnityEngine.Assertions.Assert.AreEqual(BaseResponse.ResponseEnum.SUCCESS, x.Response);
// Signal that work is finished.
bool s = autoResetEvent.Set();
Debug.Log(s);
};
// Does the request
InitData.NewUserData.Email = InitData.GetRandomEmail();
Vizzario.Instance.CreateUser(InitData.NewUserData);
// Wait for work method to signal.
bool a = autoResetEvent.WaitOne(TimeSpan.FromSeconds(5), true);
Debug.Log(a);
}
So when I run this test the result from WaitOne executes before executing the body of my MyClass.Instance.OnCreateUserSuccess and it prints false.
So it prints something like: 1. false, 2. TestCreateUser: Success
When it should print something like: 1. TestCreateUser: Success, 2. true
Why is the waitone not working, I try increasing autoResetEvent.WaitOne(TimeSpan.FromSeconds(5), true); to autoResetEvent.WaitOne(TimeSpan.FromSeconds(60), true); but I got the same results I know as a fact that the CreateUser method doesn't take more than 1 or 2 seconds so if I try autoResetEvent.WaitOne(); my test hangs and I have to restart the ide.
Any ideas.
Thanks in advance!
As requested I'm adding the method that is called in my vizzario class:
public delegate void CreateUserCallback(CreateUserResponse response);
public event CreateUserCallback OnCreateUserSuccess;
public event CreateUserCallback OnCreateUserError;
public void CreateUser(NewUserData data)
{
CreateUserResponse response = new CreateUserResponse();
this.DefApi.UserPost(data, ApiKey, ToolkitVersion, dryRun)
.Then(x =>
{
response.TokenResponse = x;
response.Response = BaseResponse.ResponseEnum.SUCCESS;
DoCreateUserNext(response);
})
.Catch(e =>
{
response.Response = BaseResponse.ResponseEnum.ERROR;
});
}
/// <summary>
/// Return callback in case of an exception inside the sdk.
/// </summary>
/// <param name="e"></param>
private void DoCreateUserError(Exception e)
{
if (OnCreateUserError != null)
{
CreateUserResponse response = new CreateUserResponse();
response.Response = BaseResponse.ResponseEnum.ERROR;
response.message = e.Message;
OnCreateUserError(response);
}
}
/// <summary>
/// Return callback from the sdk to the developer.
/// </summary>
/// <param name="response"></param>
private void DoCreateUserNext(CreateUserResponse response)
{
if (OnCreateUserSuccess != null)
{
if (response.Response == BaseResponse.ResponseEnum.SUCCESS)
{
OnCreateUserSuccess(response);
}
}
if (OnCreateUserError != null)
{
if (response.Response == BaseResponse.ResponseEnum.ERROR)
{
OnCreateUserError(response);
}
}
}

How to wait until item goes through pipeline?

So, I'm trying to wrap my head around Microsoft's Dataflow library. I've built a very simple pipeline consisting of just two blocks:
var start = new TransformBlock<Foo, Bar>();
var end = new ActionBlock<Bar>();
start.LinkTo(end);
Now I can asynchronously process Foo instances by calling:
start.SendAsync(new Foo());
What I do not understand is how to do the processing synchronously, when needed. I thought that waiting on SendAsync would be enough:
start.SendAsync(new Foo()).Wait();
But apparently it returns as soon as item is accepted by first processor in pipeline, and not when item is fully processed. So is there a way to wait until given item was processed by last (end) block? Apart from passing a WaitHandle through entire pipeline.
In short that's not supported out of the box in data flow. Essentially what you need to do is to tag the data so you can retrieve it when processing is done. I've written up a way to do this that let's the consumer await a Job as it gets processed by the pipeline. The only concession to pipeline design is that each block take a KeyValuePair<Guid, T>. This is the basic JobManager and the post I wrote about it. Note the code in the post is a bit dated and needs some updates but it should get you in the right direction.
namespace ConcurrentFlows.DataflowJobs {
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
/// <summary>
/// A generic interface defining that:
/// for a specified input type => an awaitable result is produced.
/// </summary>
/// <typeparam name="TInput">The type of data to process.</typeparam>
/// <typeparam name="TOutput">The type of data the consumer expects back.</typeparam>
public interface IJobManager<TInput, TOutput> {
Task<TOutput> SubmitRequest(TInput data);
}
/// <summary>
/// A TPL-Dataflow based job manager.
/// </summary>
/// <typeparam name="TInput">The type of data to process.</typeparam>
/// <typeparam name="TOutput">The type of data the consumer expects back.</typeparam>
public class DataflowJobManager<TInput, TOutput> : IJobManager<TInput, TOutput> {
/// <summary>
/// It is anticipated that jobHandler is an injected
/// singleton instance of a Dataflow based 'calculator', though this implementation
/// does not depend on it being a singleton.
/// </summary>
/// <param name="jobHandler">A singleton Dataflow block through which all jobs are processed.</param>
public DataflowJobManager(IPropagatorBlock<KeyValuePair<Guid, TInput>, KeyValuePair<Guid, TOutput>> jobHandler) {
if (jobHandler == null) { throw new ArgumentException("Argument cannot be null.", "jobHandler"); }
this.JobHandler = JobHandler;
if (!alreadyLinked) {
JobHandler.LinkTo(ResultHandler, new DataflowLinkOptions() { PropagateCompletion = true });
alreadyLinked = true;
}
}
private static bool alreadyLinked = false;
/// <summary>
/// Submits the request to the JobHandler and asynchronously awaits the result.
/// </summary>
/// <param name="data">The input data to be processd.</param>
/// <returns></returns>
public async Task<TOutput> SubmitRequest(TInput data) {
var taggedData = TagInputData(data);
var job = CreateJob(taggedData);
Jobs.TryAdd(job.Key, job.Value);
await JobHandler.SendAsync(taggedData);
return await job.Value.Task;
}
private static ConcurrentDictionary<Guid, TaskCompletionSource<TOutput>> Jobs {
get;
} = new ConcurrentDictionary<Guid, TaskCompletionSource<TOutput>>();
private static ExecutionDataflowBlockOptions Options {
get;
} = GetResultHandlerOptions();
private static ITargetBlock<KeyValuePair<Guid, TOutput>> ResultHandler {
get;
} = CreateReplyHandler(Options);
private IPropagatorBlock<KeyValuePair<Guid, TInput>, KeyValuePair<Guid, TOutput>> JobHandler {
get;
}
private KeyValuePair<Guid, TInput> TagInputData(TInput data) {
var id = Guid.NewGuid();
return new KeyValuePair<Guid, TInput>(id, data);
}
private KeyValuePair<Guid, TaskCompletionSource<TOutput>> CreateJob(KeyValuePair<Guid, TInput> taggedData) {
var id = taggedData.Key;
var jobCompletionSource = new TaskCompletionSource<TOutput>();
return new KeyValuePair<Guid, TaskCompletionSource<TOutput>>(id, jobCompletionSource);
}
private static ExecutionDataflowBlockOptions GetResultHandlerOptions() {
return new ExecutionDataflowBlockOptions() {
MaxDegreeOfParallelism = Environment.ProcessorCount,
BoundedCapacity = 1000
};
}
private static ITargetBlock<KeyValuePair<Guid, TOutput>> CreateReplyHandler(ExecutionDataflowBlockOptions options) {
return new ActionBlock<KeyValuePair<Guid, TOutput>>((result) => {
RecieveOutput(result);
}, options);
}
private static void RecieveOutput(KeyValuePair<Guid, TOutput> result) {
var jobId = result.Key;
TaskCompletionSource<TOutput> jobCompletionSource;
if (!Jobs.TryRemove(jobId, out jobCompletionSource)) {
throw new InvalidOperationException($"The jobId: {jobId} was not found.");
}
var resultValue = result.Value;
jobCompletionSource.SetResult(resultValue);
}
}
}
I ended up using the following pipeline:
var start = new TransformBlock<FooBar, FooBar>(...);
var end = new ActionBlock<FooBar>(item => item.Complete());
start.LinkTo(end);
var input = new FooBar {Input = new Foo()};
start.SendAsync(input);
input.Task.Wait();
Where
class FooBar
{
public Foo Input { get; set; }
public Bar Result { get; set; }
public Task<Bar> Task { get { return _taskSource.Task; } }
public void Complete()
{
_taskSource.SetResult(Result);
}
private TaskCompletionSource<Bar> _taskSource = new TaskCompletionSource<Bar>();
}
Less than ideal, but it works.

System.Net.WebSockets.ClientWebSocket no reliably working?

I am implementing passing a web socket back from our api according to this url here;
http://blogs.msdn.com/b/youssefm/archive/2012/07/17/building-real-time-web-apps-with-asp-net-webapi-and-websockets.aspx
Now the idea is that the user will register for a web socket. This is done and works using the following code;
[HttpGet]
[Route("getsocket")]
public HttpResponseMessage GetWebSocket()
{
HttpContext.Current.AcceptWebSocketRequest(new TestWebSocketHandler());
return Request.CreateResponse(HttpStatusCode.SwitchingProtocols);
}
Then they make a call to the api to begin doing some specific functions, which will then report with message back down the same websocket.
private static WebSocketWrapper _socket;
[HttpGet]
[Route("begin")]
public async Task<IHttpActionResult> StartRunning(string itemid)
{
try
{
if (_socket == null ||
_socket.State() == WebSocketState.Aborted ||
_socket.State() == WebSocketState.Closed ||
_socket.State() == WebSocketState.None)
{
_socket = WebSocketWrapper.Create("wss://localhost:44301/api/v1/testcontroller/getsocket");
_socket.OnConnect(OnConnect);
_socket.OnDisconnect(OnDisconnect);
_socket.OnMessage(OnMessage);
await _socket.ConnectAsync();
}
//builds first message to be sent and sends it
_socket.QueueMessage(firstTest);
}
catch(Exception ex)
{
//logs error
}
return Ok();
}
So effectively the client cretes a new websocket connected to the server. They then call the second message to trigger the server to start a number of tests on the device passed. The server start the tests and broadcast the messages back down the socket (the json message model contains the deviceid, so the client can filter for relevent messages).
When they receive a message the client will then acknowledge it and the next test is done etc.
Now it works the first time I run it (after compilation). However, I want to be able to have multiple client connect to the websocket list (the solution is tabulated and the tests it will run may take a while, so its possible multiple tests will be ran at any one time). So I think it has something to do with the static WebSocketWrapper instance.
However, they have asked that a single websocket be used on the server, with a list of the devices being listened for. So in effect all messages are sent to all clients from the one server connection. The clients then filter out the messages they want to listen to based on the deviceid they pass.
When I try re-running, ore running a second test, Which is basically calling the getwebsocket and then the begin method, the code runs without error, but the onopen method never gets called? Its as though the socket just doesnt fire up?
Unfortunately we cannot use signalr as this is not specified
For reference the socket wrapper class is
public class WebSocketWrapper
{
private const int ReceiveChunkSize = 2048;
private const int SendChunkSize = 2048;
private readonly ClientWebSocket _ws;
private readonly Uri _uri;
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private readonly CancellationToken _cancellationToken;
private Action<WebSocketWrapper> _onConnected;
private Action<TestResultModel, WebSocketWrapper> _onMessage;
private Action<WebSocketWrapper> _onDisconnected;
//private static Queue<TestResultModel> _messageQueue = new Queue<TestResultModel>();
private static BlockingCollection<TestResultModel> _messageQueue = new BlockingCollection<TestResultModel>();
protected WebSocketWrapper(string uri)
{
_ws = new ClientWebSocket();
_ws.Options.KeepAliveInterval = TimeSpan.FromSeconds(30);
_uri = new Uri(uri);
_cancellationToken = _cancellationTokenSource.Token;
}
/// <summary>
/// Creates a new instance.
/// </summary>
/// <param name="uri">The URI of the WebSocket server.</param>
/// <returns></returns>
public static WebSocketWrapper Create(string uri)
{
return new WebSocketWrapper(uri);
}
/// <summary>
/// Get the current state of the socket
/// </summary>
/// <returns>WebSocketState of the current _ws</returns>
public WebSocketState State()
{
return _ws.State;
}
/// <summary>
/// Disconnects from the websocket
/// </summary>
public async Task DisconnectAsync()
{
try
{
await _ws.CloseOutputAsync(
WebSocketCloseStatus.NormalClosure,
"Server has been closed by the disconnect method",
_cancellationToken);
CallOnDisconnected();
}
catch(Exception ex)
{
throw ex;
}
finally
{
_ws.Dispose();
}
}
/// <summary>
/// Connects to the WebSocket server.
/// </summary>
/// <returns></returns>
public async Task<WebSocketWrapper> ConnectAsync()
{
try
{
await _ws.ConnectAsync(_uri, _cancellationToken);
}
catch(Exception ex)
{
}
CallOnConnected();
RunInTask(() => ProcessQueueAsync());
RunInTask(() => StartListen());
return this;
}
/// <summary>
/// Set the Action to call when the connection has been established.
/// </summary>
/// <param name="onConnect">The Action to call.</param>
/// <returns></returns>
public WebSocketWrapper OnConnect(Action<WebSocketWrapper> onConnect)
{
_onConnected = onConnect;
return this;
}
/// <summary>
/// Set the Action to call when the connection has been terminated.
/// </summary>
/// <param name="onDisconnect">The Action to call</param>
/// <returns></returns>
public WebSocketWrapper OnDisconnect(Action<WebSocketWrapper> onDisconnect)
{
_onDisconnected = onDisconnect;
return this;
}
/// <summary>
/// Adds a message to the queu for sending
/// </summary>
/// <param name="message"></param>
public void QueueMessage(TestResultModel message)
{
//_messageQueue.Enqueue(message);
_messageQueue.Add(message);
}
/// <summary>
/// returns the size of the current message queue.
/// Usefult for detemning whether or not an messages are left in queue before closing
/// </summary>
/// <returns></returns>
public int QueueCount()
{
return _messageQueue.Count;
}
/// <summary>
/// Processes the message queue in order
/// </summary>
public async Task ProcessQueueAsync()
{
try
{
foreach(var current in _messageQueue.GetConsumingEnumerable())
{
await SendMessageAsync(current);
}
}
catch(Exception ex)
{
//TODO
}
}
/// <summary>
/// Set the Action to call when a messages has been received.
/// </summary>
/// <param name="onMessage">The Action to call.</param>
/// <returns></returns>
public WebSocketWrapper OnMessage(Action<TestResultModel, WebSocketWrapper> onMessage)
{
_onMessage = onMessage;
return this;
}
/// <summary>
/// Send a message to the WebSocket server.
/// </summary>
/// <param name="message">The message to send</param>
public async Task SendMessageAsync(TestResultModel result)
{
if (_ws.State != WebSocketState.Open)
{
throw new Exception("Connection is not open.");
}
var message = JsonConvert.SerializeObject(result);
var messageBuffer = Encoding.UTF8.GetBytes(message);
var messagesCount = (int)Math.Ceiling((double)messageBuffer.Length / SendChunkSize);
for (var i = 0; i < messagesCount; i++)
{
var offset = (SendChunkSize * i);
var count = SendChunkSize;
var lastMessage = ((i + 1) == messagesCount);
if ((count * (i + 1)) > messageBuffer.Length)
{
count = messageBuffer.Length - offset;
}
await _ws.SendAsync(new ArraySegment<byte>(messageBuffer, offset, count), WebSocketMessageType.Text, lastMessage, _cancellationToken);
}
}
private async Task StartListen()
{
var buffer = new byte[ReceiveChunkSize];
//part of a big hack, temporary solution
string prevResult = "";
try
{
while (_ws.State == WebSocketState.Open)
{
var stringResult = new StringBuilder();
WebSocketReceiveResult result;
do
{
result = await _ws.ReceiveAsync(new ArraySegment<byte>(buffer), _cancellationToken);
if (result.MessageType == WebSocketMessageType.Close)
{
await _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
CallOnDisconnected();
}
else
{
var str = Encoding.UTF8.GetString(buffer, 0, result.Count);
stringResult.Append(str);
}
} while (!result.EndOfMessage);
if (!prevResult.Equals(stringResult.ToString()))
{
prevResult = stringResult.ToString();
CallOnMessage(stringResult);
}
}
}
catch (Exception ex)
{
CallOnDisconnected();
}
finally
{
_ws.Dispose();
}
}
private void CallOnMessage(StringBuilder stringResult)
{
if (_onMessage != null)
{
try
{
var message = JsonConvert.DeserializeObject<TestResultModel>(stringResult.ToString());
RunInTask(() => _onMessage(message, this));
}
catch (Exception ex)
{
//Ignore any other messages not TestResultModel
}
}
}
private void CallOnDisconnected()
{
if (_onDisconnected != null)
RunInTask(() => _onDisconnected(this));
}
private void CallOnConnected()
{
if (_onConnected != null)
RunInTask(() => _onConnected(this));
}
private static Task RunInTask(Action action)
{
return Task.Factory.StartNew(action);
}
}
As an update please see the debug screen taken when trying to call the websocket for the second time. As you can see it appears to be in the aborted state? (on first run obviousl its null). Any ideas?
A lot of the functionality you are looking is "easy" to implement using SignalR. As where you create a "Hub" where you can broadcast to all clients or simply choose a single client.
I take it that you have previous experience with websockets, so I think you should take a look! It is a bit magical and can be a hell to troubleshoot in bigger applications and services.
A simple chat tutorial can be found here
You can also take a look here for a person that has had problems sending notications to single clients using SignalR

How can I make method signature caching?

I'm building an app in .NET and C#, and I'd like to cache some of the results by using attributes/annotations instead of explicit code in the method.
I'd like a method signature that looks a bit like this:
[Cache, timeToLive=60]
String getName(string id, string location)
It should make a hash based on the inputs, and use that as the key for the result.
Naturally, there'd be some config file telling it how to actually put in memcached, local dictionary or something.
Do you know of such a framework?
I'd even be interested in one for Java as well
With CacheHandler in Microsoft Enterprise Library you can easily achieve this.
For instance:
[CacheHandler(0, 30, 0)]
public Object GetData(Object input)
{
}
would make all calls to that method cached for 30 minutes. All invocations gets a unique cache-key based on the input data and method name so if you call the method twice with different input it doesn't get cached but if you call it >1 times within the timout interval with the same input then the method only gets executed once.
I've added some extra features to Microsoft's code:
My modified version looks like this:
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.Remoting.Contexts;
using System.Text;
using System.Web;
using System.Web.Caching;
using System.Web.UI;
using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;
using Microsoft.Practices.Unity.InterceptionExtension;
namespace Middleware.Cache
{
/// <summary>
/// An <see cref="ICallHandler"/> that implements caching of the return values of
/// methods. This handler stores the return value in the ASP.NET cache or the Items object of the current request.
/// </summary>
[ConfigurationElementType(typeof (CacheHandler)), Synchronization]
public class CacheHandler : ICallHandler
{
/// <summary>
/// The default expiration time for the cached entries: 5 minutes
/// </summary>
public static readonly TimeSpan DefaultExpirationTime = new TimeSpan(0, 5, 0);
private readonly object cachedData;
private readonly DefaultCacheKeyGenerator keyGenerator;
private readonly bool storeOnlyForThisRequest = true;
private TimeSpan expirationTime;
private GetNextHandlerDelegate getNext;
private IMethodInvocation input;
public CacheHandler(TimeSpan expirationTime, bool storeOnlyForThisRequest)
{
keyGenerator = new DefaultCacheKeyGenerator();
this.expirationTime = expirationTime;
this.storeOnlyForThisRequest = storeOnlyForThisRequest;
}
/// <summary>
/// This constructor is used when we wrap cached data in a CacheHandler so that
/// we can reload the object after it has been removed from the cache.
/// </summary>
/// <param name="expirationTime"></param>
/// <param name="storeOnlyForThisRequest"></param>
/// <param name="input"></param>
/// <param name="getNext"></param>
/// <param name="cachedData"></param>
public CacheHandler(TimeSpan expirationTime, bool storeOnlyForThisRequest,
IMethodInvocation input, GetNextHandlerDelegate getNext,
object cachedData)
: this(expirationTime, storeOnlyForThisRequest)
{
this.input = input;
this.getNext = getNext;
this.cachedData = cachedData;
}
/// <summary>
/// Gets or sets the expiration time for cache data.
/// </summary>
/// <value>The expiration time.</value>
public TimeSpan ExpirationTime
{
get { return expirationTime; }
set { expirationTime = value; }
}
#region ICallHandler Members
/// <summary>
/// Implements the caching behavior of this handler.
/// </summary>
/// <param name="input"><see cref="IMethodInvocation"/> object describing the current call.</param>
/// <param name="getNext">delegate used to get the next handler in the current pipeline.</param>
/// <returns>Return value from target method, or cached result if previous inputs have been seen.</returns>
public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
lock (input.MethodBase)
{
this.input = input;
this.getNext = getNext;
return loadUsingCache();
}
}
public int Order
{
get { return 0; }
set { }
}
#endregion
private IMethodReturn loadUsingCache()
{
//We need to synchronize calls to the CacheHandler on method level
//to prevent duplicate calls to methods that could be cached.
lock (input.MethodBase)
{
if (TargetMethodReturnsVoid(input) || HttpContext.Current == null)
{
return getNext()(input, getNext);
}
var inputs = new object[input.Inputs.Count];
for (int i = 0; i < inputs.Length; ++i)
{
inputs[i] = input.Inputs[i];
}
string cacheKey = keyGenerator.CreateCacheKey(input.MethodBase, inputs);
object cachedResult = getCachedResult(cacheKey);
if (cachedResult == null)
{
var stopWatch = Stopwatch.StartNew();
var realReturn = getNext()(input, getNext);
stopWatch.Stop();
if (realReturn.Exception == null && realReturn.ReturnValue != null)
{
AddToCache(cacheKey, realReturn.ReturnValue);
}
return realReturn;
}
var cachedReturn = input.CreateMethodReturn(cachedResult, input.Arguments);
return cachedReturn;
}
}
private object getCachedResult(string cacheKey)
{
//When the method uses input that is not serializable
//we cannot create a cache key and can therefore not
//cache the data.
if (cacheKey == null)
{
return null;
}
object cachedValue = !storeOnlyForThisRequest ? HttpRuntime.Cache.Get(cacheKey) : HttpContext.Current.Items[cacheKey];
var cachedValueCast = cachedValue as CacheHandler;
if (cachedValueCast != null)
{
//This is an object that is reloaded when it is being removed.
//It is therefore wrapped in a CacheHandler-object and we must
//unwrap it before returning it.
return cachedValueCast.cachedData;
}
return cachedValue;
}
private static bool TargetMethodReturnsVoid(IMethodInvocation input)
{
var targetMethod = input.MethodBase as MethodInfo;
return targetMethod != null && targetMethod.ReturnType == typeof (void);
}
private void AddToCache(string key, object valueToCache)
{
if (key == null)
{
//When the method uses input that is not serializable
//we cannot create a cache key and can therefore not
//cache the data.
return;
}
if (!storeOnlyForThisRequest)
{
HttpRuntime.Cache.Insert(
key,
valueToCache,
null,
System.Web.Caching.Cache.NoAbsoluteExpiration,
expirationTime,
CacheItemPriority.Normal, null);
}
else
{
HttpContext.Current.Items[key] = valueToCache;
}
}
}
/// <summary>
/// This interface describes classes that can be used to generate cache key strings
/// for the <see cref="CacheHandler"/>.
/// </summary>
public interface ICacheKeyGenerator
{
/// <summary>
/// Creates a cache key for the given method and set of input arguments.
/// </summary>
/// <param name="method">Method being called.</param>
/// <param name="inputs">Input arguments.</param>
/// <returns>A (hopefully) unique string to be used as a cache key.</returns>
string CreateCacheKey(MethodBase method, object[] inputs);
}
/// <summary>
/// The default <see cref="ICacheKeyGenerator"/> used by the <see cref="CacheHandler"/>.
/// </summary>
public class DefaultCacheKeyGenerator : ICacheKeyGenerator
{
private readonly LosFormatter serializer = new LosFormatter(false, "");
#region ICacheKeyGenerator Members
/// <summary>
/// Create a cache key for the given method and set of input arguments.
/// </summary>
/// <param name="method">Method being called.</param>
/// <param name="inputs">Input arguments.</param>
/// <returns>A (hopefully) unique string to be used as a cache key.</returns>
public string CreateCacheKey(MethodBase method, params object[] inputs)
{
try
{
var sb = new StringBuilder();
if (method.DeclaringType != null)
{
sb.Append(method.DeclaringType.FullName);
}
sb.Append(':');
sb.Append(method.Name);
TextWriter writer = new StringWriter(sb);
if (inputs != null)
{
foreach (var input in inputs)
{
sb.Append(':');
if (input != null)
{
//Diffrerent instances of DateTime which represents the same value
//sometimes serialize differently due to some internal variables which are different.
//We therefore serialize it using Ticks instead. instead.
var inputDateTime = input as DateTime?;
if (inputDateTime.HasValue)
{
sb.Append(inputDateTime.Value.Ticks);
}
else
{
//Serialize the input and write it to the key StringBuilder.
serializer.Serialize(writer, input);
}
}
}
}
return sb.ToString();
}
catch
{
//Something went wrong when generating the key (probably an input-value was not serializble.
//Return a null key.
return null;
}
}
#endregion
}
}
Microsoft deserves most credit for this code. We've only added stuff like caching at request level instead of across requests (more useful than you might think) and fixed some bugs (e.g. equal DateTime-objects serializing to different values).
To do exactly what you are describing, i.e. writing
public class MyClass {
[Cache, timeToLive=60]
string getName(string id, string location){
return ExpensiveCall(id, location);
}
}
// ...
MyClass c = new MyClass();
string name = c.getName("id", "location");
string name_again = c.getName("id", "location");
and having only one invocation of the expensive call and without needing to wrap the class with some other code (f.x. CacheHandler<MyClass> c = new CacheHandler<MyClass>(new MyClass());) you need to look into an Aspect Oriented Programming framework. Those usually work by rewriting the byte-code, so you need to add another step to your compilation process - but you gain a lot of power in the process. There are many AOP-frameworks, but PostSharp for .NET and AspectJ are among the most popular. You can easily Google how to use those to add the caching-aspect you want.

Categories

Resources