Using async in ASP.Net MVC - c#

I had the following action which hung and never returned:
public Task<ActionResult> ManageProfile(ManageProfileMessageId? message)
{
ViewBag.StatusMessage =
message == ManageProfileMessageId.ChangeProfileSuccess
? "Your profile has been updated."
: message == ManageProfileMessageId.Error
? "An error has occurred."
: "";
ViewBag.ReturnUrl = Url.Action("ManageProfile");
var user = UserManager.FindByIdAsync(User.Identity.GetUserId());
var profileModel = new UserProfileViewModel
{
Email = user.Email,
City = user.City,
Country = user.Country
};
return View(profileModel);
}
but when I converted it to this:
public async Task<ActionResult> ManageProfile(ManageProfileMessageId? message)
{
ViewBag.StatusMessage =
message == ManageProfileMessageId.ChangeProfileSuccess
? "Your profile has been updated."
: message == ManageProfileMessageId.Error
? "An error has occurred."
: "";
ViewBag.ReturnUrl = Url.Action("ManageProfile");
var user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
var profileModel = new UserProfileViewModel
{
Email = user.Email,
City = user.City,
Country = user.Country
};
return View(profileModel);
}
It returned right away. So Im not sure what is going on with this? If its as simple as the method returned without waiting on the result of FindByIdAsync, then why didn't I get a view with nothing in it.
So it appears to me that it neither waited for the return of:
UserManager.FindByIdAsync(User.Identity.GetUserId());
nor returned a null profile nor threw an exception. So I dont get whats happening here when it's hanging in the first example.

I assume your first example was using Result, thus causing a deadlock that I explain on my blog.
In summary, ASP.NET provides a "request context" which only allows one thread in at a time. When you block a thread using Result, that thread is locked into that context. Later, when FindByIdAsync attempts to resume on that context, it cannot because there's another thread already blocked in it.

Related

Multiple messages to the bot in quick succession crash it

Setup
I have a a bot that runs on .NET + Bot Framework + Azure + Facebook Messenger.
Initial Problem
I was trying to solve a problem when sending several messages to the bot triggers an exception and HTTP error 412. Microsoft describes this problem here: https://learn.microsoft.com/en-us/bot-framework/troubleshoot-general-problems#what-causes-an-error-with-http-status-code-412-precondition-failed-or-http-status-code-409-conflict
First Solution
In the page above, Microsoft provides an outdated sample code to resolve this issue. In this github issue, there is a revised version of that code that is supposed to work. I put it inside the constructor of my MessageController:
static MessagesController()
{
// Prevent exception in the bot and HTTP error 412 when the user
// sends multiple messages in quick succession. This may cause
// potential problems with consistency of getting/setting user
// properties.
// See https://learn.microsoft.com/en-us/bot-framework/troubleshoot-general-problems#what-causes-an-error-with-http-status-code-412-precondition-failed-or-http-status-code-409-conflict
// for details. The above link contains wrong code sample, revised
// code is from here: https://github.com/Microsoft/BotBuilder/issues/2345
var builder = new ContainerBuilder();
builder
.Register(c => new CachingBotDataStore(c.ResolveKeyed<IBotDataStore<BotData>>(typeof(ConnectorStore)), CachingBotDataStoreConsistencyPolicy.LastWriteWins))
.As<IBotDataStore<BotData>>()
.AsSelf()
.InstancePerLifetimeScope();
builder.Update(Conversation.Container);
}
Second Problem
Now, the exception still occurs when I send several messages to the bot in a quick succession. However, it changed from HTTP error 412 to something else:
One or more errors occurred. at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions) at System.Threading.Tasks.Task1.GetResultCore(Boolean waitCompletionNotification) at System.Threading.Tasks.Task1.get_Result() at MyBot.SetUserDataProperty(Activity activity, String PropertyName, String ValueToSet) in C:\Users\xxx.cs:line 230
Update: I've checked the InnerException of the above and it turns out to be the same old HTTP error 412:
The remote server returned an error: (412) Precondition Failed.
The offending code is a function that writes to the bot storage. The line 230 referenced above is the last line of this function:
public static void SetUserDataProperty(Activity activity, string PropertyName, string ValueToSet)
{
StateClient client = activity.GetStateClient();
BotData userData = client.BotState.GetUserData(activity.ChannelId, activity.From.Id);
userData.SetProperty<string>(PropertyName, ValueToSet);
//client.BotState.SetUserDataAsync(activity.ChannelId, activity.From.Id, userData);
// Await async call without making the function asynchronous:
var temp = Task.Run(() => client.BotState.SetUserDataAsync(activity.ChannelId, activity.From.Id, userData)).Result;
}
Question
What else can I do to make sure that the user is able to send multiple messages in quick succession without triggering an exception when writing to the BotState storage?
I think there are a few issues here
The way you are trying to do this activity.GetStateClient(); is a only intended to be used for prototyping. We do no reccomend this method for production level code. You can set user data like context.UserData.SetValue("food", "Nachos" ); in the dialog and the values will automagically get saved when the dialog is serialized.
Most likely you are calling this method SetUserDataProperty from a dialog so when you do this var temp = Task.Run(() => client.BotState.SetUserDataAsync(activity.ChannelId, activity.From.Id, userData)).Result; it is conflicting and causing the error.
please review this blog post to learn more
Here is how to implement your follow up question:
if (activity.Type == ActivityTypes.Message)
{
var message = activity as IMessageActivity;
using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, message))
{
var botDataStore = scope.Resolve<IBotDataStore<BotData>>();
var key = new AddressKey()
{
BotId = message.Recipient.Id,
ChannelId = message.ChannelId,
UserId = message.From.Id,
ConversationId = message.Conversation.Id,
ServiceUrl = message.ServiceUrl
};
ConversationReference r = new ConversationReference();
var userData = await botDataStore.LoadAsync(key, BotStoreType.BotUserData, CancellationToken.None);
userData.SetProperty("key 1", "value1");
userData.SetProperty("key 2", "value2");
await botDataStore.SaveAsync(key, BotStoreType.BotUserData, userData, CancellationToken.None);
await botDataStore.FlushAsync(key, CancellationToken.None);
}
await Conversation.SendAsync(activity, () => new Dialogs.RootDialog());
}
you will need to implement this class or something similar:
public class AddressKey : IAddress
{
public string BotId { get; set; }
public string ChannelId { get; set; }
public string ConversationId { get; set; }
public string ServiceUrl { get; set; }
public string UserId { get; set; }
}

Send email from async method

If I'm trying to send an email immediately a new email has been confirmed, how to I go about it ?
The piece of code below is what I have in ConfirmEmail method. When I run the code, I get Exception Details: System.NullReferenceException: Object reference not set to an instance of an object. on await UserManager.SendEmailAsync(userId, "API", "API Code is: " + user.ApiCode);
I think UserManager.FindById is not returning anything, but not sure how to go about it.
Any pointers/ideas will help.
public async Task<ActionResult> ConfirmEmail(string userId, string code)
{
if (userId == null || code == null)
{
return View("Error");
}
var result = await UserManager.ConfirmEmailAsync(userId, code);
var user = UserManager.FindById(User.Identity.GetUserId());
await UserManager.SendEmailAsync(userId, "API", "API Code is: " + user.ApiCode);
return View(result.Succeeded ? "ConfirmEmail" : "Error");
}
With the info you provided I guess maybe the problem is that the user is not authenticated and therefore User.Identity will return null, hence the NullReferenceException.
You could try to fetch the user information with the id you're receiving in the action parameter userId instead.
public async Task<ActionResult> ConfirmEmail(string userId, string code)
{
if (userId == null || code == null)
{
return View("Error");
}
var result = await UserManager.ConfirmEmailAsync(userId, code);
//var user = UserManager.FindById(User.Identity.GetUserId());
var user = UserManager.FindById(userId); //use the received parameter to get the user
await UserManager.SendEmailAsync(userId, "API", "API Code is: " + user.ApiCode);
return View(result.Succeeded ? "ConfirmEmail" : "Error");
}
Hope this helps!

Why does my UpdateAsync(user) method fail the first time, claiming user doesn't exist?

I have a method that signs a user in and attempts to update the user if the viewModel is different than user object. The behaviour I'm seeing is confusing.
Every time the method executes, if the user was not previously logged in, the line await _userManager.UpdateAsync(user); fails with the exception: "There is no user with id: 99" (or whatever id value). However, if the user was previously logged in, the line works.
So for example,
User starts up the app (not currently logged in) and clicks button to post viewModel to server.
The app logs the user in,
If the viewModel is different than the existing user's data, the server attempts to update it.
This update will fail with "There is no user with id: 99"
User clicks button again and posts the same data to the server. (this time user has been logged in from previous failed post)
viewModel is still different than existing data (remember, the update failed last time)
await _userManager.UpdateAsync(user); works and the record is updated.
The following is the method:
[UnitOfWork]
public async Task<AjaxResponse> Post(MyViewModel viewModel)
{
try
{
var loginResult = await _userManager.LoginAsync(viewModel.UserName, viewModel.Password, viewModel.TenancyName);
User user;
if (loginResult.Result == AbpLoginResultType.Success)
{
await SignInAsync(loginResult.User, loginResult.Identity);
user = loginResult.User;
if (user.AccessToken != viewModel.AccessToken)
{
user.AccessToken = viewModel.AccessToken;
// why does this fail the first time?
await _userManager.UpdateAsync(user);
}
}
else
{
/* do some other UnitOfWork stuff below */
}
return new AjaxResponse(new MyResult
{
Name = user.Name + " " + user.Surname,
UserName = user.UserName,
EmailAddress = user.EmailAddress,
IsActive = user.IsActive,
Success = true,
UserId = user.UserId,
});
}
catch (Exception ex)
{
throw new HttpException((int)HttpStatusCode.InternalServerError, ex.Message);
}
}
I can confirm that a user with the id 99 does in fact exist in the database.
For the record, the following is the contents of ex.StackTrace
at Abp.Authorization.Users.AbpUserManager`3.<GetUserByIdAsync>d__5b.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Abp.Authorization.Users.AbpUserManager`3.<UpdateAsync>d__64.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at MyProject.Web.Controllers.Api.AccountApiController.<Post>d__16.MoveNext() in C:\dev\MyProject\MyProject.Web\Controllers\Api\AccountApiController.cs:line 146
I think one clue might be in the following query (intercepted with SQL Server Profiler) that gets executed before the update:
exec sp_executesql N'SELECT TOP (1)
[Extent1].[Id] AS [Id],
[Extent1].[AccessToken] AS [AccessToken],
[Extent1].[UserId] AS [UserId],
[Extent1].[EmailAddress] AS [EmailAddress],
[Extent1].[TenantId] AS [TenantId],
[Extent1].[IsDeleted] AS [IsDeleted],
-- irrelevant stuff removed
FROM [dbo].[AbpUsers] AS [Extent1]
WHERE
((([Extent1].[TenantId] IS NULL) AND (#DynamicFilterParam_1 IS NULL))
OR (([Extent1].[TenantId] IS NOT NULL) AND ([Extent1].[TenantId] = #DynamicFilterParam_1))
OR (#DynamicFilterParam_2 IS NOT NULL)) AND (([Extent1].[IsDeleted] = #DynamicFilterParam_3)
OR (#DynamicFilterParam_4 IS NOT NULL)) AND ([Extent1].[EmailAddress] = #p__linq__0)',
N'#DynamicFilterParam_1 int,#DynamicFilterParam_2 bit,#DynamicFilterParam_3 bit,#DynamicFilterParam_4 bit,#p__linq__0 nvarchar(4000)'
,#DynamicFilterParam_1=NULL,#DynamicFilterParam_2=NULL,#DynamicFilterParam_3=0,#DynamicFilterParam_4=NULL,#p__linq__0=N'myemail#mail.com'
Here, we can see that #DynamicFilterParam_1=NULL. The variable #DynamicFilterParam_1 corresponds to the value for [Extent1].[TenantId]. If I manually assign the value 2 (which is the value associated with the record in the db) instead of NULL and rerun the query, it returns the record as I expect.
When I execute the method the second time, I can see that TenantId is correctly assigned the value of 2.
Why is the value corresponding to TenantId being assigned NULL the first time? Why does the UpdateAsync method fail every first time? What can I do to make it work?
In response to the request below, the definition of UpdateAsync is available in the asp.net boilerplate github
This is because of multitenancy. ABP does not know the TenantId before user login. You should manually Set tenant id to switch to the tenant of the user. My Login code is like that:
public virtual async Task<JsonResult> Login(LoginViewModel loginModel, string returnUrl = "", string returnUrlHash = "")
{
var loginResult = await GetLoginResultAsync(loginModel.UsernameOrEmailAddress, loginModel.Password, loginModel.TenancyName);
var tenantId = loginResult.Tenant == null ? (int?)null : loginResult.Tenant.Id;
using (UnitOfWorkManager.Current.SetTenantId(tenantId))
{
if (loginResult.User.ShouldChangePasswordOnNextLogin)
{
loginResult.User.SetNewPasswordResetCode();
return Json(new AjaxResponse
{
TargetUrl = Url.Action(
"ResetPassword",
new ResetPasswordViewModel
{
TenantId = tenantId,
UserId = SimpleStringCipher.Instance.Encrypt(loginResult.User.Id.ToString()),
ResetCode = loginResult.User.PasswordResetCode
})
});
}
var signInResult = await _signInManager.SignInOrTwoFactorAsync(loginResult, loginModel.RememberMe);
if (signInResult == SignInStatus.RequiresVerification)
{
return Json(new AjaxResponse
{
TargetUrl = Url.Action(
"SendSecurityCode",
new
{
returnUrl = returnUrl + (returnUrlHash ?? ""),
rememberMe = loginModel.RememberMe
})
});
}
Debug.Assert(signInResult == SignInStatus.Success);
await UnitOfWorkManager.Current.SaveChangesAsync();
if (string.IsNullOrWhiteSpace(returnUrl))
{
returnUrl = GetAppHomeUrl();
}
if (!string.IsNullOrWhiteSpace(returnUrlHash))
{
returnUrl = returnUrl + returnUrlHash;
}
return Json(new AjaxResponse { TargetUrl = returnUrl });
}
}
The critical thing is the using (UnitOfWorkManager.Current.SetTenantId(tenantId)) statement

null reference error in task.run async method

I am using the following code for sign in in a web api. I get null reference exception on FormsAuthentication.SetAuthCookie(authUser.UserId.ToString(), false);
call. Please guide me what I am doing wrong...
[AllowAnonymous]
[HttpPost]
public async Task<string> SignIn(JObject credentails)
{
string returnVal = "";
await Task.Run(() =>
{
string userName = (string)credentails.SelectToken("Username");
string password = (string)credentails.SelectToken("Password");
UserService userSvc = new UserService(new SqlConnection(_conStr));
var authUser = userSvc.Authenticate(userName, password);
if (authUser != null)
{
FormsAuthentication.SetAuthCookie(userName, false);
HttpContext.Current.Session.Add("DR_CLIENT_ID", authUser.DRClientId);
HttpContext.Current.Session.Add("USER_ID", authUser.UserId);
returnVal = authUser.FullName;
}
else
{
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
{
Content = new StringContent("Invalid Credentials!"),
ReasonPhrase = "Error"
});
}
});
return returnVal;
}
UPDATE-1
in this case no value is actually null as I can see it in the debug mode. but when I remove wait Task.Run(() = {}); block from this code, it works fine without any issue.
The problem is Task.Run. In ASP.NET, when an incoming request arrives, it assigns a thread pool thread to handle that request, and this thread runs your code. What your code then does is use Task.Run to move to another thread pool thread without a request context, and then assumes it has a request context. FormsAuthentication.SetAuthCookie (and HttpContext.Current) will simply not work without a request context.
To resolve this, remove the call to Task.Run. You should (almost) never use Task.Run on ASP.NET.

How can I return a BadRequest on Azure Mobile Services TableController GET template method?

I am using Azure Mobile Services (following the standard Azure TodoItems tutorial), and the most basic GET method that they provide is:
public IQueryable<MyModel> GetAllMyInfo()
{
return Query();
}
This works, but I am trying to extend it so that the method will only return MyModel data for an authenticated user (identified by the X-ZUMO-AUTH authentication header standard for Mobile Service API calls). So I modified the code for:
public IQueryable<MyModel> GetAllMyInfo()
{
// Get the current user
var currentUser = User as ServiceUser;
var ownerId = currentUser.Id;
return Query().Where(s => s.OwnerId == ownerId);
}
This also works when a valid auth token is passed. However, if an invalid auth header is passed, then the currentUser is null, and the query fails (obviously). So I am trying to check for null and return a BadRequest or a 403 HTTP code. Yet a simple `return BadRequest("Invalid authentication") gives a compilation error:
public IQueryable<MyModel> GetAllMyInfo()
{
// Get the current user
var currentUser = User as ServiceUser;
if(currentUser == null) {
return BadRequest("Database has already been created."); // This line gives a compilation error saying I need a cast.
}
var ownerId = currentUser.Id;
return Query().Where(s => s.OwnerId == ownerId);
}
Does anyone know how to check for a valid authentication token and return a 403 on this method (which wants an IQueryable return type?
You can use the [AuthorizeLevel] attribute on this method to indicate that a valid token must be present in order for the method to be invoked. It will return a 401 if not.
So your full method would be:
[AuthorizeLevel(AuthorizationLevel.User)]
public IQueryable<MyModel> GetAllMyInfo()
{
// Get the current user
var currentUser = User as ServiceUser;
var ownerId = currentUser.Id;
return Query().Where(s => s.OwnerId == ownerId);
}
Please note that for the Azure Mobile Apps SDK (not Mobile Services), the above attribute is simply replaced with [Authorize].
I know this is a bit late, but will document here for you and others that may come looking for a similar problem.
(While agreeing with Matt that a 403 could/should be achieved with a [Authorize] attribute, the question is regarding returning a different HttpStatusCode OR IQueryable)
I had a similar scenario where I needed to validate some query parameters and either return my results or a HttpError (in my case I wanted a 404 with content).
I found 2 ways, either keeping the return as IQueryable<T> and throwing a HttpResponseException or changing the return to IHttpActionResult and returning normal with HttpStatusCode or Ok(Data).
I found to prefer the later as throwing an Exception would be breaking the execution while in debug and not a very pleasant development experience.
Option 1 (Preferred)
//Adding Return annotation for API Documentation generation
[ResponseType(typeof(IQueryable<MyModel>))]
public IHttpActionResult GetAllMyInfo()
{
// Get the current user
var currentUser = User as ServiceUser;
if(currentUser == null) {
return BadRequest("Database has already been created.");
}
var ownerId = currentUser.Id;
return Ok(Query().Where(s => s.OwnerId == ownerId));
}
Option 2 (Throwing Exception)
public IQueryable<MyModel> GetAllMyInfo()
{
// Get the current user
var currentUser = User as ServiceUser;
if(currentUser == null) {
throw new HttpResponseException(System.Net.HttpStatusCode.BadRequest)
// Or to add a content message:
throw new HttpResponseException(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.BadRequest) {
Content = new System.Net.Http.StringContent("Database has already been created.")
});
}
var ownerId = currentUser.Id;
return Query().Where(s => s.OwnerId == ownerId);
}

Categories