I have modified one of #slodge's samples for a problem I have with my ViewModels lifecycle.
I've modified N26 a little:
https://github.com/csteeg/NPlus1DaysOfMvvmCross/tree/viewmodeldisposesample/N-26-Fraggle
This branch uses the mvxmessenger plugin to be able to show you where things go wrong.
The code isn't pretty, but shows you wat is incorrect. You can see how SubViewModel with Id = 0 keeps receiving messsages, even when it's view is long gone. And also (at some point) how HomeViewModel stops receiving messages.
Steps to reproduce (including a cleaned up version of the debug output):
start the app
HomeViewModel:Warning:HomeViewModel 0 received: Created HomeViewModel0
Click on button '1'
HomeViewModel:Warning:HomeViewModel 0 received: Created SubViewModel0
SubViewModel:Warning:SubViewModel 0 received: Created SubViewModel0
HomeViewModel:Warning:HomeViewModel 0 received: Created FirstViewModel0
SubViewModel:Warning:SubViewModel 0 received: Created FirstViewModel0
FirstViewModel:Warning:FirstViewModel 0 received: Created FirstViewModel0
Click on back
HomeViewModel:Warning:HomeViewModel 0 received: Destroyed FirstView for viewmodel 0
SubViewModel:Warning:SubViewModel 0 received: Destroyed FirstView for viewmodel 0
FirstViewModel:Warning:FirstViewModel 0 received: Destroyed FirstView for viewmodel 0
HomeViewModel:Warning:HomeViewModel 0 received: Destroyed SubFrag for viewmodel 0
SubViewModel:Warning:SubViewModel 0 received: Destroyed SubFrag for viewmodel 0
FirstViewModel:Warning:FirstViewModel 0 received: Destroyed SubFrag for viewmodel 0
HomeViewModel:Warning:HomeViewModel 0 received: Destroyed DubFrag for viewmodel 0
SubViewModel:Warning:SubViewModel 0 received: Destroyed DubFrag for viewmodel 0
FirstViewModel:Warning:FirstViewModel 0 received: Destroyed DubFrag for viewmodel 0
You can see the views getting destroyed here, I'd expect the viewmodels to go with them
Click button '1' again
HomeViewModel:Warning:HomeViewModel 0 received: Created SubViewModel1
SubViewModel:Warning:SubViewModel 0 received: Created SubViewModel1
FirstViewModel:Warning:FirstViewModel 0 received: Created SubViewModel1
SubViewModel:Warning:SubViewModel 1 received: Created SubViewModel1
HomeViewModel:Warning:HomeViewModel 0 received: Created FirstViewModel1
SubViewModel:Warning:SubViewModel 0 received: Created FirstViewModel1
FirstViewModel:Warning:FirstViewModel 0 received: Created FirstViewModel1
SubViewModel:Warning:SubViewModel 1 received: Created FirstViewModel1
FirstViewModel:Warning:FirstViewModel 1 received: Created FirstViewModel1
Here you see, subviewmodel 0 is still receiving messages.
Can I somehow tell it should stop sending messages to viewmodels not attached?
OR could the viewmodel know about not being attached
Now, as you continue to to repeat these steps for quite some time, say 15 times in the emulator,
some viewmodels will stop receiving messages (I guess they're garbage collected).
Strange thing is that one of those views is HomeViewModel! The HomeView is never destroyed, yet the homeviewmdoel stops receiving messages,
thus nog being able to update the view accordingly if you app requires that
I had a similar situation, where MvxMessages were still being received and acted on by viewmodels which were no longer attached to views.
My solution is to add the following to the base viewmodel:
a Subscribe method
a list of unsubscribe actions (this is added to by the Subscribe method)
an UnsubscribeAll method
And in the Android activity OnDestoy, I call the viewmodel's UnsubscribeAll.
(As a bonus, as the tokens are referenced in the unsubscribe actions, I don't need to keep another list of them)
BaseViewModel:
#region Messenger
/// <summary>
/// Must set the Messenger object before doing any subscribing
/// </summary>
public IMvxMessenger Messenger { get; set; }
private readonly object _messengerLock = new Object();
private List<Action> _unsubscribeActions;
/// <summary>
/// Subscribe to a message, and store in a list so can be unsubscribed automatically later
/// </summary>
/// <typeparam name="TMessage"></typeparam>
/// <param name="deliveryAction"></param>
public void Subscribe<TMessage>(Action<TMessage> deliveryAction) where TMessage: MvxMessage
{
var messenger = Messenger;
if (messenger == null) { return; }
var token = messenger.Subscribe<TMessage>(deliveryAction);
Action unsubscriber = delegate()
{
messenger.Unsubscribe<TMessage>(token);
};
lock (_messengerLock)
{
if (_unsubscribeActions == null)
{
_unsubscribeActions = new List<Action>();
}
_unsubscribeActions.Add(unsubscriber);
}
}
/// <summary>
/// Unsubscribe to all messages which have been previously subscribed to
/// </summary>
public void UnsubscribeAll()
{
if (_unsubscribeActions == null) { return; }
lock (_messengerLock)
{
foreach (var a in _unsubscribeActions)
{
a();
}
_unsubscribeActions = null;
}
}
#endregion
BaseActivity:
protected override void OnDestroy()
{
var vm = ViewModel as ViewModel.BaseViewModel;
if (vm != null) { vm.UnsubscribeAll(); }
base.OnDestroy();
}
MvvmCross v3 doesn't expose the ViewModel to any View lifecycle events like ViewDidAppear/Disappear, OnNavigatedTo/From, OnPause/OnResume/OnDestroy.
The reason for this is because:
earlier MvvmCross versions tried to do this and came unstuck with iOS changes in ViewDidUnload and with iOS support in general (it was hard to work out when a view was dead for good)
when Views are not pages - but are instead tabs, flyouts, splitviews, popups, etc - then it was hard to support and confusing to developers
Instead, MvvmCross v3 makes use of Garbage Collection to tidy up the ViewModels. To assist with this Mvx always uses WeakReferences from ViewModel-to-View and from MessageHub-to-ViewModels. The general philosophy followed is:
the only thing that keeps a strong reference on a View is the operating system.
the only thing that keeps a strong reference on a ViewModel is its View.
With that said, if GarbageCollection isn't timely enough for your app, then MvvmCross does allow you to extend your ViewModels with new functionality. For example, you could easily add a new IViewLifecycleAware interface if you want to. This is easy to do, but once done then it's your app's responsibility to ensure that interface is called from appropriate View events/overrides on each platform you support.
There's a little more on this topic in:
https://github.com/slodge/MvvmCross/wiki/View-Model-Lifecycle#viewmodel-deactivation-activation-and-destruction
ViewModel LifeCycle, when does it get disposed?
https://github.com/slodge/MvvmCross/issues/74
For your specific problem with HomeViewModel stopping receiving messages, I think that is due to you not storing the subscription token.
Because the MvvmCross messenger uses weak referencing by default, you must store the subscription token - when that token is disposed or garbage collected then the subscription will be unsubscribed.
So your code:
public class HomeViewModel
: MvxViewModel
{
public static int IdCounter = 0;
public int Id = IdCounter++;
public HomeViewModel()
{
var messenger = Mvx.Resolve<IMvxMessenger>();
messenger.Subscribe<JustAMessage>(OnMessage);
messenger.Publish(new JustAMessage(this) { Message = "Created HomeViewModel" + Id });
}
// ...
needs to be:
public class HomeViewModel
: MvxViewModel
{
public static int IdCounter = 0;
public int Id = IdCounter++;
private IDisposable _token;
public HomeViewModel()
{
var messenger = Mvx.Resolve<IMvxMessenger>();
_token = messenger.Subscribe<JustAMessage>(OnMessage);
messenger.Publish(new JustAMessage(this) { Message = "Created HomeViewModel" + Id });
}
// ...
There's more about this in:
https://github.com/slodge/MvvmCross/wiki/MvvmCross-plugins#messenger
Related
I am working on an application that needs to recognize when someone log's off from system.
I've set ping interval to 5 seconds. I also have public override Task OnConnected() and public override Task OnDisconnected(), and they are triggered each time client connects and disconnects (I remember clients names and connections while connecting, and remove those data from list when disconnected)
So, when a client log's off the system, his connection is lost and he doesn't send any ping response. I have this solution working, but when it takes too much time, around 30 seconds to run OnDisconnected function.
Is there any possibility to make this process faster? I have to detect this in at least 4-5 seconds.
EDIT: here is the code
Hub.cs:
public class SignalRConnection
{
public string ConnectionId { get; set; }
public string UserName { get; set; }
}
public static List<SignalRConnection> connections = new List<SignalRConnection>();
public override Task OnConnected()
{
connections.Add(new SignalRConnection() { ConnectionId = Context.ConnectionId, UserName=Context.User.Identity.Name});
return base.OnConnected();
}
public override Task OnDisconnected()
{
var forDelete = connections.FirstOrDefault(p => p.ConnectionId == Context.ConnectionId);
connections.Remove(forDelete);
Clients.All.logoffActions(forDelete.UserName);
return base.OnDisconnected();
}
Global.asax
GlobalHost.Configuration.ConnectionTimeout =TimeSpan.FromSeconds(6);
GlobalHost.Configuration.DisconnectTimeout = TimeSpan.FromSeconds(6);
GlobalHost.Configuration.KeepAlive = TimeSpan.FromSeconds(2);
You can detect what happens from javascript. This will help understanding what happens with the connection: http://www.asp.net/signalr/overview/guide-to-the-api/handling-connection-lifetime-events
Basically first, your connection goes in reconnecting state (can be slow connection, short interruption in connection ... ) In this state tries to connect again. By default 6 times every 5 seconds, which takes 30 seconds. If no success fires the disconnected event which is going to call your hub's OnDisconnected method.
You can use the reconnecting function in javascript, but in this state, client can still come back.
You can use something like this is JS:
var tryingToReconnect = false;
$.connection.hub.reconnecting(function() {
tryingToReconnect = true;
});
$.connection.hub.reconnected(function() {
tryingToReconnect = false;
});
$.connection.hub.disconnected(function() {
if(tryingToReconnect) {
notifyUserOfDisconnect(); // Your function to notify user.
}
});
When the connection is down $.connection.hub.reconnecting(function() { will be called instantly, (but the firing of this event doesn't always mean that the connection is down! It might just be a quick connection problem)
Another useful thing would be the connectionSlow event.
$.connection.hub.connectionSlow(function() {
notifyUserOfConnectionProblem(); // Your function to notify user.
});
Most importantly to understand the states and the transitions described in the linked description.
I hope it helps. Cheers!
This is another strange problem I've encountered this days!!! I've created and MVC 4 app using nhibernate. and added a filter attribute named [LoggingNHibernateSessionAttribute] on my HomeController which manages session for each action. I've followed 'ASP.NET MVC4 and the Web API published by Apress'.
public class LoggingNHibernateSessionAttribute : ActionFilterAttribute
{
private readonly IActionLogHelper _actionLogHelper;
private readonly IActionExceptionHandler _actionExceptionHandler;
private readonly IActionTransactionHelper _actionTransactionHelper;
public LoggingNHibernateSessionAttribute()
: this(WebContainerManager.Get<IActionLogHelper>(),
WebContainerManager.Get<IActionExceptionHandler>(),
WebContainerManager.Get<IActionTransactionHelper>())
{
}
public LoggingNHibernateSessionAttribute(
IActionLogHelper actionLogHelper,
IActionExceptionHandler actionExceptionHandler,
IActionTransactionHelper actionTransactionHelper)
{
_actionLogHelper = actionLogHelper;
_actionExceptionHandler = actionExceptionHandler;
_actionTransactionHelper = actionTransactionHelper;
}
public override void OnActionExecuting(ActionExecutingContext actionExectingContext)
{
_actionLogHelper.LogEntry(actionExectingContext.ActionDescriptor);
_actionTransactionHelper.BeginTransaction();
}
public override void OnActionExecuted(ActionExecutedContext actionExecutedContext)
{
_actionTransactionHelper.EndTransaction(actionExecutedContext);
_actionTransactionHelper.CloseSession();
_actionExceptionHandler.HandleException(actionExecutedContext);
_actionLogHelper.LogExit(actionExecutedContext.ActionDescriptor);
}
}
ActionTransactionHelper
public class ActionTransactionHelper : IActionTransactionHelper
{
private readonly ISessionFactory _sessionFactory;
private readonly ICurrentSessionContextAdapter _currentSessionContextAdapter;
public ActionTransactionHelper(
ISessionFactory sessionFactory,
ICurrentSessionContextAdapter currentSessionContextAdapter)
{
_sessionFactory = sessionFactory;
_currentSessionContextAdapter = currentSessionContextAdapter;
}
public void BeginTransaction()
{
var session = _sessionFactory.GetCurrentSession();
if (session != null)
{
session.BeginTransaction();
}
}
public bool TransactionHandled { get; private set; }
public void EndTransaction(ActionExecutedContext filterContext)
{
var session = _sessionFactory.GetCurrentSession();
if (session == null) return;
if (!session.Transaction.IsActive) return;
if (filterContext.Exception == null)
{
session.Flush();
session.Transaction.Commit();
}
else
{
session.Transaction.Rollback();
}
TransactionHandled = true;
}
public bool SessionClosed { get; private set; }
public void CloseSession()
{
if (_currentSessionContextAdapter.HasBind(_sessionFactory))
{
var session = _sessionFactory.GetCurrentSession();
session.Close();
session.Dispose();
_currentSessionContextAdapter.Unbind(_sessionFactory);
SessionClosed = true;
}
}
}
when run the app, I can save an entity in the dataBase. but when I hit refresh button and exception thrown indication session is closed.
I don't know why this happens. (I searched and find this NHibernate throwing Session is closed but couldn't solve my problem).
in my NinjectConfigurator I added inRequestScope() to all of injections but no answer. I checked when I refresh the page session will be opened. but I donnow why it say session is closed?!
UPDATE:
when I first run the app. I can create a new member. but when I hit the refresh button, the session will be closed unexpectedly!!
first run:
everything works well
after hitting refresh button:
a new session bind to the current context.
the new session will be injected the repository (session is open)
the ActionTransactionHelper calls beginTransaction()
4- customMembership createUser (....) called
5- but when the _userRepositoy.save(user)called in the repository session is closed!!!!
note:but when still endTransaction and closeSession isn't called. but how session is closed?
if I comment closeSession() in onActionExecute(). session alway is open and everything woks well if refresh the page.
I checked a lot and tried different way I knew. it only happens when for the second time I want to do CRUD operations with my customMembership.
for other entities it works like a charm!
I have upoaded my sample code. for testing just create and empty database and change connection string. then go to localHost:*****/api/categories (user and pass doesn't required)
Download sample project:
Size: 47 MB
https://www.dropbox.com/s/o63wjng5f799fii/Hashem-MVC4ServicesBook.rar
size: 54 MB
Zip Format: https://www.dropbox.com/s/smrsbz4cbtznx1y/Hashem-MVC4ServicesBook2.zip
A very important thing here, could be the nature of the NHibernate. The NHibernate and its Session are in the ASP.NET MVC living longer, then could be expected. I mean not only inside of the
ActionExecuting (Controller Action starts)
ActionExecuted (the View or Redirect is called)
Session in fact must live also through the phase of rendering. Because, we could load some proxy in the "Action()" but its collection, could be lazily loaded only during the View rendering. So even in these phases Session must be opened (the same Session from the request begining)
ResultExecuting (the proxy could start to be loaded only here)
ResultExecuted (almost all is done, let's close the session)
Other words... keep the session opened throught the complete Request. From authorization untill the content is rendered.
NOTE: Anohter hint, just to be sure that all is ok, I am using this scenario (maybe you do as well):
Client FORM is about to send the data to server. The method is POST, the Action is Update()
Sent FORM is coming to server, Action Update() is triggerred - all the transactions stuff is in place (as described above)
Once NHibernate persists the data into DB, the Update() action ends, and is redirected to action
Detail() if all is ok or
Edit() if something goes wrong
The users Browser was redirected to action Detail or Edit. So if user does REFRESH, the Detail or Edit is refreshed. The Update() is not called at all (it is a POST method)
In fact, the step 1. was one of the Actions Detail or Edit. In this case, we would face this issue already...
You have this error since Asp.Net MVC does not create a new instance of LoggingNHibernateSessionAttribute every request. It creates a new instance when you request an action first time and then uses this instance in the future.
The behaviour is the following:
First invocation of Post -> new instance of 'LoggingNHibernateSession' is created
First invocation of Put -> another one instance of 'LoggingNHibernateSession' is created
Second invocation of Put -> instance of 'LoggingNHibernateSession' from previous step is used
First invocation of Delete -> another one instance of 'LoggingNHibernateSession' is created
[LoggingNHibernateSession]
public JsonResult Post(Dto data)
{
/* ... */
}
[LoggingNHibernateSession]
public JsonResult Put(int id, Dto data)
{
/* ... */
}
[LoggingNHibernateSession]
public JsonResult Delete(int id)
{
/* ... */
}
It can be solved using Func<IActionLogHelper> instead of IActionLogHelper in the constructor. An instance of IActionLogHelper can be initialised within OnActionExecuting method.
public class LoggingNHibernateSessionAttribute : ActionFilterAttribute
{
/* your code */
private readonly Func<IActionTransactionHelper> _getActionTransactionHelper;
private IActionTransactionHelper _actionTransactionHelper;
public LoggingNHibernateSessionAttribute()
: this(WebContainerManager.Get<IActionLogHelper>(),
WebContainerManager.Get<IActionExceptionHandler>(),
() => WebContainerManager.Get<IActionTransactionHelper>())
{
}
public LoggingNHibernateSessionAttribute(
IActionLogHelper actionLogHelper,
IActionExceptionHandler actionExceptionHandler,
Func<IActionTransactionHelper> getActionTransactionHelper)
{
_actionLogHelper = actionLogHelper;
_actionExceptionHandler = actionExceptionHandler;
_getActionTransactionHelper = getActionTransactionHelper;
_actionTransactionHelper = null;
}
public override void OnActionExecuting(ActionExecutingContext actionExectingContext)
{
_actionTransactionHelper = _getActionTransactionHelper();
_actionLogHelper.LogEntry(actionExectingContext.ActionDescriptor);
_actionTransactionHelper.BeginTransaction();
}
/* your code */
}
I am trying to set up a lightweight HTML5 Server-Sent Event implementation on my MVC 4 Web, without using one of the libraries available to implement sockets and similars.
The lightweight approach I am trying is:
Client side:
EventSource (or jquery.eventsource for IE)
Server side:
long polling with AsynchController (sorry for dropping here the raw test code but just to give an idea)
public class HTML5testAsyncController : AsyncController
{
private static int curIdx = 0;
private static BlockingCollection<string> _data = new BlockingCollection<string>();
static HTML5testAsyncController()
{
addItems(10);
}
//adds some test messages
static void addItems(int howMany)
{
_data.Add("started");
for (int i = 0; i < howMany; i++)
{
_data.Add("HTML5 item" + (curIdx++).ToString());
} _data.Add("ended");
}
// here comes the async action, 'Simple'
public void SimpleAsync()
{
AsyncManager.OutstandingOperations.Increment();
Task.Factory.StartNew(() =>
{
var result = string.Empty; var sb = new StringBuilder();
string serializedObject = null;
//wait up to 40 secs that a message arrives
if (_data.TryTake(out result, TimeSpan.FromMilliseconds(40000)))
{
JavaScriptSerializer ser = new JavaScriptSerializer();
serializedObject = ser.Serialize(new { item = result, message = "MSG content" });
sb.AppendFormat("data: {0}\n\n", serializedObject);
}
AsyncManager.Parameters["serializedObject"] = serializedObject;
AsyncManager.OutstandingOperations.Decrement();
});
}
// callback which returns the results on the stream
public ActionResult SimpleCompleted(string serializedObject)
{ ServerSentEventResult sar = new ServerSentEventResult();
sar.Content = () => { return serializedObject; };
return sar;
}
//pushes the data on the stream in a format conforming HTML5 SSE
public class ServerSentEventResult : ActionResult
{
public ServerSentEventResult() { }
public delegate string GetContent();
public GetContent Content { get; set; }
public int Version { get; set; }
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
} if (this.Content != null)
{
HttpResponseBase response = context.HttpContext.Response;
// this is the content type required by chrome 6 for server sent events
response.ContentType = "text/event-stream";
response.BufferOutput = false; // this is important because chrome fails with a "failed to load resource" error if the server attempts to put the char set after the content type
response.Charset = null;
string[] newStrings = context.HttpContext.Request.Headers.GetValues("Last-Event-ID");
if (newStrings == null || newStrings[0] != this.Version.ToString())
{
string value = this.Content();
response.Write(string.Format("data:{0}\n\n", value));
//response.Write(string.Format("id:{0}\n", this.Version));
}
else
{
response.Write("");
}
}
}
}
}
The problem is on the server side as there is still a big gap between the expected result and what's actually going on.
Expected result:
EventSource opens a stream connection to the server,
the server keeps it open for a safe time (say, 2 minutes) so that I am protected from thread leaking from dead clients,
as new message events are received by the server (and enqueued to a thread safe collection such as BlockingCollection) they are pushed in the open stream to the client:
message 1 received at T+0ms, pushed to the client at T+x
message 2 received at T+200ms, pushed to the client at T+x+200ms
Actual behaviour:
EventSource opens a stream connection to the server,
the server keeps it open until a message event arrives (thanks to long polling)
once a message is received, MVC pushes the message and closes the connection.
EventSource has to reopen the connection and this happens after a couple of seconds.
message 1 received at T+0ms, pushed to the client at T+x
message 2 received at T+200ms, pushed to the client at T+x+3200ms
This is not OK as it defeats the purpose of using SSE as the clients start again reconnecting as in normal polling and message delivery gets delayed.
Now, the question:
is there a native way to keep the connection open after sending the first message and sending further messages on the same connection?
Instead of relying on SimpleComplete to send the data, you want to send the data using Response.Flush. By doing an AsyncManager.OutstandingOperations.Decrement(), you're telling the AsyncController that you're finished processing the request and you're ready to send the response and close the connection. Instead, you avoid calling OutStandingOperations.Decrement() until the connection is lost, etc. Whenever you want to push a message to the client, you directly call Response.Write and Response.Flush from some background thread. Also, AsyncControllers have a default timeout after which they automatically close the connection. To get around that, you'll want to use the NoAsyncTimeoutAttribute for the relevant actions.
As a side note, AsyncController's interface doesn't really allow a clean way of implementing a SSE stream; I would have personally implemented a HttpTaskAsyncHandler given that I was using Asp.NET MVC 4.
Summary
I need to retrieve attachments stored in a parent app from a link in a client of a child app. The attachments are available in the parent app via a web service call -- which returns a standard FileContentResult with content type "application/octet-stream". The best way I can think is to retrieve this via a WebRequest and pass the resulting response stream to a FileStreamResult, though I have some alternatives available.
Does anyone know if, when making a WebRequest, the response stream becomes available immediately once the first part of the response is returned or is it buffered so I don't get the response until all data has been retrieved?
Are there any other options than those listed in the full question below for doing this that I'm missing? (Other than keeping the attachments in both child and parent DBs -- I really don't want to do this since then I'd need to regularly synchronize them, too).
TLDR Version
I have two related applications which communicate through a RESTful web service. The parent application maintains a collection of entities which may have attachments. For example, a Request might have an Excel spreadsheet as an attachment. The entity and its attachment are stored in the database and access to the attachment is controlled using the same logic as access to the Request. That is, you should not be able to download an attachment if you cannot view the Request.
In the child application I maintain some integration glue for the entities assigned to a particular institution -- the app is used to communicate between our Board of Regents and each Regents school. I don't want to maintain and synchronize the full entity/attachment. I only want to maintain enough information to allow me to connect to the web service in the parent app and get the details for entities that the particular instance of the child application has access to.
This works well for the entity data itself. The amount of data is small and the overhead of buffering in the child application doesn't present a signficant delay in accessing the data. If necessary, I could cache the data locally to avoid performance penalities.
My concern is the attachments. I've considered three different mechanisms for providing access to the attachment from a client of the child application.
Generate a one-time use token and associated url that allows the client to directly download the attachment from the parent application. The token generation web service call would ensure that users of the child application should have access to the attachment. The drawback to this is that you'd only be able to click on the link once in the client. Clicking again would result in an error rather than getting the attachment.
Buffer the attachment in the child app. In this scenario I would provide a controller/action to download the attachment in the child app, then call a web service method to get the attachment and have the child app send the attachment as a FileContentResult. This removes the issue of only being able to click the link once, but the attachments could be reasonably large and buffering the data in the child application could potentially double the amount of time to download the attachment and, worse, incur a significant delay before the attachment download begins.
Link in the child app, but provide the stream from the web service request directly to a FileStreamResult. This seems, to me, to be the best option as the FileStreamResult reads in chunks rather than having to have all the data available before it is sent to the client. The only drawback that I can see here is that I can no longer dispose of the WebResponse directly as the FileStreamResult won't be executed until after my action returns.
Here is what I have for the code for API wrapper code for (2) and (3):
private class ResponseModel<T> : IDisposable
{
public T Model { get; set; }
public WebResponse Response { get; set; }
private bool Disposed { get; set; }
private void Dispose( bool disposing )
{
if (!Disposed)
{
if (disposing)
{
((IDisposable)this.Response).Dispose();
}
Disposed = true;
}
}
public void Dispose()
{
Dispose( true );
}
}
private ResponseModel<T> GetAttachmentResponse<T>( long id ) where T : IDownloadModel, new()
{
var request = GetRequest( string.Format( "{0}/api/getattachment/{1}/{2}", this.BaseUrl, this.Key, id ) );
var response = request.GetResponse();
var model = (T)Activator.CreateInstance<T>();
var contentDisposition = response.Headers["Content-Disposition"];
if (!string.IsNullOrEmpty( contentDisposition ))
{
var filename = contentDisposition.Split( new[] { ';', ' ' }, StringSplitOptions.RemoveEmptyEntries )
.SingleOrDefault( s => s.StartsWith( "filename", StringComparison.OrdinalIgnoreCase ) );
if (!string.IsNullOrEmpty( filename ))
{
model.Name = filename.Split( '=' ).Skip( 1 ).FirstOrDefault();
}
}
if (string.IsNullOrEmpty( model.Name ))
{
model.Name = "untitled";
}
return new ResponseModel<T> { Model = model, Response = response };
}
public FileDownloadModel GetAttachment( long id )
{
using (var response = GetAttachmentResponse<FileDownloadModel>( id ))
{
var reader = new BinaryReader( response.Response.GetResponseStream() );
response.Model.Content = reader.ReadBytes( (int)response.Response.ContentLength );
return response.Model;
}
}
public FileStreamDownloadModel GetAttachmentStream( long id )
{
// since we're returning the stream, we can't dispose of the response when done.
var response = GetAttachmentResponse<FileStreamDownloadModel>( id );
response.Model.Stream = response.Response.GetResponseStream();
return response.Model;
}
public interface IDownloadModel
{
string ContentType { get; }
string Name { get; set; }
}
Model classes
public class FileDownloadModel : IDownloadModel
{
public byte[] Content { get; set; }
public string Name { get; set; }
public string ContentType { get { return "application/octet-stream"; } }
}
public class FileStreamDownloadModel : IDownloadModel
{
public Stream Stream { get; set; }
public string Name { get; set; }
public string ContentType { get { return "application/octet-stream"; } }
}
I would suggest a variant on Option 1 [call it Option 1(a)].
Instead of generating a one-time token, "borrow" the MVC AntiForgeryToken classes, and have your parent application return a custom token and cookie to the child app for inclusion in the form returned to the user.
If the child application may have links for multiple documents on a single page, in the request for the token information, have the child app submit a unique identifier (identifying the page request from the user) as part of the request. You can then use this identifier in generating the tokens, and you can store the identifier as part of the verification process. This will give you a multi-use token, unique for each link on the page.
Slap an expiration time on the unique identifier, and you should be good to go.
We're experimenting with various ways to throttle user actions in a given time period:
Limit question/answer posts
Limit edits
Limit feed retrievals
For the time being, we're using the Cache to simply insert a record of user activity - if that record exists if/when the user does the same activity, we throttle.
Using the Cache automatically gives us stale data cleaning and sliding activity windows of users, but how it will scale could be a problem.
What are some other ways of ensuring that requests/user actions can be effectively throttled (emphasis on stability)?
Here's a generic version of what we've been using on Stack Overflow for the past year:
/// <summary>
/// Decorates any MVC route that needs to have client requests limited by time.
/// </summary>
/// <remarks>
/// Uses the current System.Web.Caching.Cache to store each client request to the decorated route.
/// </remarks>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class ThrottleAttribute : ActionFilterAttribute
{
/// <summary>
/// A unique name for this Throttle.
/// </summary>
/// <remarks>
/// We'll be inserting a Cache record based on this name and client IP, e.g. "Name-192.168.0.1"
/// </remarks>
public string Name { get; set; }
/// <summary>
/// The number of seconds clients must wait before executing this decorated route again.
/// </summary>
public int Seconds { get; set; }
/// <summary>
/// A text message that will be sent to the client upon throttling. You can include the token {n} to
/// show this.Seconds in the message, e.g. "Wait {n} seconds before trying again".
/// </summary>
public string Message { get; set; }
public override void OnActionExecuting(ActionExecutingContext c)
{
var key = string.Concat(Name, "-", c.HttpContext.Request.UserHostAddress);
var allowExecute = false;
if (HttpRuntime.Cache[key] == null)
{
HttpRuntime.Cache.Add(key,
true, // is this the smallest data we can have?
null, // no dependencies
DateTime.Now.AddSeconds(Seconds), // absolute expiration
Cache.NoSlidingExpiration,
CacheItemPriority.Low,
null); // no callback
allowExecute = true;
}
if (!allowExecute)
{
if (String.IsNullOrEmpty(Message))
Message = "You may only perform this action every {n} seconds.";
c.Result = new ContentResult { Content = Message.Replace("{n}", Seconds.ToString()) };
// see 409 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
c.HttpContext.Response.StatusCode = (int)HttpStatusCode.Conflict;
}
}
}
Sample usage:
[Throttle(Name="TestThrottle", Message = "You must wait {n} seconds before accessing this url again.", Seconds = 5)]
public ActionResult TestThrottle()
{
return Content("TestThrottle executed");
}
The ASP.NET Cache works like a champ here - by using it, you get automatic clean-up of your throttle entries. And with our growing traffic, we're not seeing that this is an issue on the server.
Feel free to give feedback on this method; when we make Stack Overflow better, you get your Ewok fix even faster :)
Microsoft has a new extension for IIS 7 called Dynamic IP Restrictions Extension for IIS 7.0 - Beta.
"The Dynamic IP Restrictions for IIS 7.0 is a module that provides protection against denial of service and brute force attacks on web server and web sites. Such protection is provided by temporarily blocking IP addresses of the HTTP clients who make unusually high number of concurrent requests or who make large number of requests over small period of time."
http://learn.iis.net/page.aspx/548/using-dynamic-ip-restrictions/
Example:
If you set the criteria to block after X requests in Y milliseconds or X concurrent connections in Y milliseconds the IP address will be blocked for Y milliseconds then requests will be permitted again.
We use the technique borrowed from this URL http://www.codeproject.com/KB/aspnet/10ASPNetPerformance.aspx, not for throttling, but for a poor man's Denial Of Service (D.O.S). This is also cache-based, and may be similar to what you are doing. Are you throttling to prevent D.O.S. attacks? Routers can certainly be used to reduce D.O.S; do you think a router could handle the throttling you need?
It took me some time to work out an equivalent for .NET 5+ (formerly .NET Core), so here's a starting point.
The old way of caching has gone and been replaced by Microsoft.Extensions.Caching.Memory with IMemoryCache.
I separated it out a bit more, so here's what you need...
The Cache Management Class
I've added the whole thing here, so you can see the using statements.
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Primitives;
using System;
using System.Threading;
namespace MyWebApplication
{
public interface IThrottleCache
{
bool AddToCache(string key, int expriryTimeInSeconds);
bool AddToCache<T>(string key, T value, int expriryTimeInSeconds);
T GetFromCache<T>(string key);
bool IsInCache(string key);
}
/// <summary>
/// A caching class, based on the docs
/// https://learn.microsoft.com/en-us/aspnet/core/performance/caching/memory?view=aspnetcore-6.0
/// Uses the recommended library "Microsoft.Extensions.Caching.Memory"
/// </summary>
public class ThrottleCache : IThrottleCache
{
private IMemoryCache _memoryCache;
public ThrottleCache(IMemoryCache memoryCache)
{
_memoryCache = memoryCache;
}
public bool AddToCache(string key, int expriryTimeInSeconds)
{
bool isSuccess = false; // Only a success if a new value gets added.
if (!IsInCache(key))
{
var cancellationTokenSource = new CancellationTokenSource(
TimeSpan.FromSeconds(expriryTimeInSeconds));
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetSize(1)
.AddExpirationToken(
new CancellationChangeToken(cancellationTokenSource.Token));
_memoryCache.Set(key, DateTime.Now, cacheEntryOptions);
isSuccess = true;
}
return isSuccess;
}
public bool AddToCache<T>(string key, T value, int expriryTimeInSeconds)
{
bool isSuccess = false;
if (!IsInCache(key))
{
var cancellationTokenSource = new CancellationTokenSource(
TimeSpan.FromSeconds(expriryTimeInSeconds));
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(DateTimeOffset.Now.AddSeconds(expriryTimeInSeconds))
.SetSize(1)
.AddExpirationToken(
new CancellationChangeToken(cancellationTokenSource.Token));
_memoryCache.Set<T>(key, value, cacheEntryOptions);
isSuccess = true;
}
return isSuccess;
}
public T GetFromCache<T>(string key)
{
return _memoryCache.Get<T>(key);
}
public bool IsInCache(string key)
{
var item = _memoryCache.Get(key);
return item != null;
}
}
}
The attribute itself
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System;
using System.Net;
namespace MyWebApplication
{
/// <summary>
/// Decorates any MVC route that needs to have client requests limited by time.
/// Based on how they throttle at stack overflow (updated for .NET5+)
/// https://stackoverflow.com/questions/33969/best-way-to-implement-request-throttling-in-asp-net-mvc/1318059#1318059
/// </summary>
/// <remarks>
/// Uses the current System.Web.Caching.Cache to store each client request to the decorated route.
/// </remarks>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class ThrottleByIPAddressAttribute : ActionFilterAttribute
{
/// <summary>
/// The caching class (which will be instantiated as a singleton)
/// </summary>
private IThrottleCache _throttleCache;
/// <summary>
/// A unique name for this Throttle.
/// </summary>
/// <remarks>
/// We'll be inserting a Cache record based on this name and client IP, e.g. "Name-192.168.0.1"
/// </remarks>
public string Name { get; set; }
/// <summary>
/// The number of seconds clients must wait before executing this decorated route again.
/// </summary>
public int Seconds { get; set; }
/// <summary>
/// A text message that will be sent to the client upon throttling. You can include the token {n} to
/// show this.Seconds in the message, e.g. "Wait {n} seconds before trying again".
/// </summary>
public string Message { get; set; } = "You may only perform this action every {n} seconds.";
public override void OnActionExecuting(ActionExecutingContext c)
{
if(_throttleCache == null)
{
var cache = c.HttpContext.RequestServices.GetService(typeof(IThrottleCache));
_throttleCache = (IThrottleCache)cache;
}
var key = string.Concat(Name, "-", c.HttpContext.Request.HttpContext.Connection.RemoteIpAddress);
var allowExecute = _throttleCache.AddToCache(key, Seconds);
if (!allowExecute)
{
if (String.IsNullOrEmpty(Message))
Message = "You may only perform this action every {n} seconds.";
c.Result = new ContentResult { Content = Message.Replace("{n}", Seconds.ToString()) };
// see 409 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
c.HttpContext.Response.StatusCode = (int)HttpStatusCode.Conflict;
}
}
}
}
Startup.cs or Program.cs - Register the services with DI
This example uses Startup.cs/ConfigureServices - Put the code somewhere after AddControllersWithViews).
For a project created in .NET6+ I think you'd add the equivalent between builder.Services.AddRazorPages(); and var app = builder.Build(); in program.cs. services would be builder.Services.
If you don't get the placement of this code right, the cache will be empty every time you check it.
// The cache for throttling must be a singleton and requires IMemoryCache to be set up.
// Place it after AddControllersWithViews or AddRazorPages as they build a cache themselves
// Need this for IThrottleCache to work.
services.AddMemoryCache(_ => new MemoryCacheOptions
{
SizeLimit = 1024, /* TODO: CHECK THIS IS THIS THE RIGHT SIZE FOR YOU! */
CompactionPercentage = .3,
ExpirationScanFrequency = TimeSpan.FromSeconds(30),
});
services.AddSingleton<IThrottleCache, ThrottleCache>();
Example Usage
[HttpGet, Route("GetTest")]
[ThrottleByIPAddress(Name = "MyControllerGetTest", Seconds = 5)]
public async Task<ActionResult<string>> GetTest()
{
return "Hello world";
}
To help understand caching in .NET 5+, I've also made a caching console demo.
Since the highly voted answers to this question are too old, I am sharing the latest solution which worked for me.
I tried using the Dynamic IP restrictions as given in an answer on this page but when I tried to use that extension, I found that this extension has been discontinued by Microsoft and on the download page they have clearly written the below message.
Microsoft has discontinued the Dynamic IP Restrictions extension and this download is no longer available.
So I researched further and found that the Dynamic IP Restrictions is now by default included in IIS 8.0 and above. The below information is fetched from the Microsoft Dynamic IP Restrictions page.
In IIS 8.0, Microsoft has expanded the built-in functionality to include several new features:
Dynamic IP address filtering, which allows administrators to
configure their server to block access for IP addresses that exceed
the specified number of requests.
The IP address filtering features now allow administrators to specify
the behavior when IIS blocks an IP address, so requests from
malicious clients can be aborted by the server instead of returning
HTTP 403.6 responses to the client.
IP filtering now feature a proxy mode, which allows IP addresses to
be blocked not only by the client IP that is seen by IIS but also by
the values that are received in the x-forwarded-for HTTP header
For step by step instructions to implement Dynamic IP Restrictions, please visit the below link:
https://learn.microsoft.com/en-us/iis/get-started/whats-new-in-iis-8/iis-80-dynamic-ip-address-restrictions
I hope it helps someone stuck in a similar problem.
Created ThrottlingTroll - my take on throttling/rate limiting in ASP.NET Core.
It is similar to Stefan Prodan's AspNetCoreRateLimit and ASP.NET 7's Rate Limiting Middleware, but has advantages:
Both ingress and egress throttling (egress means that your specially configured HttpClient won't make more than N requests per second and will instead produce 429 status code by itself).
Distributed rate counter stores (including, but not limited to Redis).
Dynamic (re)configuration - allows to adjust limits without restarting the service.
Propagating 429 statuses from egress to ingress.
Check out more in the repo.