Lock web object among users in asp.net mvc - javascript

I have an asp.net mvc site where users can select a number of objects, they are identified by a unique id.
What I would like to do is:
User A opens an object
When user B tries to open the same object, a message will appear saying this is already open by user A, therefore is inaccessible.
I guess the server would have a sort of 'shared' session that can be queried in every Ajax request, to check if the object is already open by a different user. When the object is closed then that id should be removed from the shared list of opened objects. What would be the best approach for this?

In a typical scenario, assuming that you are working with plain CLR objects, this could be a solution
// User1 and User2 are trying to access objectA
private static int objectACounter; // Counter to decide whether object A is occupied
if(objectACounter != 0) // Object A is occupied
return user message object A is occupied
lock(objectA)
{
objectACounter ++;
// User logic processing
objectACounter --;
}
Above mentioned solution can be further refined by using a static dictionary to suggest which user has currently locked the object A and at what time, which can be returned to the incoming user.
In this case you plan to use something like Shared session, in my understanding there's nothing like a shared session, since session is user specific, above mentioned mechanism is a way to create one. You can plan to use Application Cache instead, where for each object you can define a key for each object and add the details in same way as you would do to the static dictionary, since Cache is a key value pair and it persist across the multiple user requests on the server.
Logic will be something like:
if(Cache.ContainsKey(objectAKey))
// Return error to the incoming user with details of object holding user
lock(objectA)
{
Cache.Insert(objectAKey, UserValues);
// User1 logic processing
Cache.Remove(objectAKey, UserValues);
}
Please let me know if I have misunderstood your question and you are expecting a different solution, We can adapt the current solution in multiple scenarios

I had to do something similar recently, but what I ended up doing was to not prevent a second person from accessing a submitted application, but to notify them that someone else is currently working on it when they view it (passive feedback) and if they attempt to modify it, prevent them from doing so, either after the postback occurs and possibly rendering the UI in a readonly fashion (active feedback).
I tracked the activity of who was using what with SignalR, this way I could dynamically update the list of applications when someone accessed one for the first time, or release an application when they leave the page, without forcing the user viewing the list of projects to refresh the page to be made aware of the change.
I setup a signalR Hub in my code to track which project was claimed by whom. I implemented the tracking using ConcurrentDictionary class to tackle any sort of concurrency problems of modifying the collection.
public class RetainedApplicationsHub : Hub
{
private static readonly ConcurrentDictionary<Guid, RetainedApplication> RetainedApplications = new ConcurrentDictionary<Guid, RetainedApplication>();
public static bool IsApplicationRetained(Guid applicationId)
{
return RetainedApplications.ContainsKey(applicationId);
}
public void RetainApplication(Guid applicationId, Guid personId)
{
var retainedSuccessfully = RetainedApplications.TryAdd(applicationId, new RetainedApplication{ConnectionId = Context.ConnectionId, PersonId = personId});
if(retainedSuccessfully)
Clients.All.applicationRetained(applicationId);
}
public void ReleaseApplication(Guid applicationId)
{
RetainedApplication temp;
RetainedApplications.TryRemove(applicationId, out temp);
Clients.All.applicationReleased(applicationId);
}
public ICollection<Guid> GetRetainedApplicationIds()
{
return RetainedApplications.Keys;
}
public override Task OnDisconnected(bool stopCalled)
{
RetainedApplication temp;
var applicationId = RetainedApplications.Single(x => x.Value.ConnectionId == Context.ConnectionId).Key;
RetainedApplications.TryRemove(applicationId, out temp);
Clients.All.applicationReleased(applicationId);
return base.OnDisconnected(stopCalled);
}
}
public class RetainedApplication
{
public string ConnectionId { get; set; }
public Guid PersonId { get; set; }
}
On my Application list page
<script src="~/signalr/hubs"></script>
<script>
$(function () {
'use strict';
var retainedApplicationsHub = $.connection.retainedApplicationsHub;
retainedApplicationsHub.client.applicationRetained = function(applicationId) {
$("td:contains('" + applicationId + "')").parent().addClass("active text-muted");
};
retainedApplicationsHub.client.applicationReleased = function(applicationId) {
$("td:contains('" + applicationId + "')").parent().removeClass();
};
$.connection.hub.start().done(function() {
retainedApplicationsHub.server.getRetainedApplicationIds()
.done(function(applicationIds) {
$.each(applicationIds, function() {
retainedApplicationsHub.client.applicationRetained(this);
});
});
});
});
</script>
On my application details page
<script src="~/signalr/hubs"></script>
<script>
$(function () {
'use strict';
var retainedApplicationsHub = $.connection.retainedApplicationsHub;
retainedApplicationsHub.client.applicationRetained = function(applicationId) {
//do nothing, must subscribe to at least one event to have the OnDisconnected event on the hub register correctly
};
$.connection.hub.start().done(function () {
retainedApplicationsHub.server.retainApplication("#Model.RegistrationApplication.ObjectId", "#User.GetIdentityId()");
});
});
</script>

Related

What are the best practices to model database records as javascript class objects?

I am looking for a guide on how to initialize a javascript class based on a database record. For example, suppose I have a User model. Therefore, I would have a User class that 1) creates and saves a new user to the database and 2) retrieves the user and initializes it. The class that I am thinking of would look like this:
class User {
constructor(user) {
this.user = user;
}
static createUser(user) {
// makes a call to an API to store the user in a database
}
static fromDB(user) {
// user comes from a database call
this.user = user;
}
sayHello() {
console.log('hi!');
}
}
Use case 1 - store user in db:
User.createUser(user);
Use case 2 - initiate new user object
const userRecord = APICallToDbToGetUser()
const user = new User(User.fromDb(userRecord));
user.sayHello();
I wasn't sure if this was the best practice or not and could not find tutorials on the subject. Any feedback on this would really be appreciated.
Thanks!

Ways to persist SignalR connection

I am creating web application, which let's users to communicate with so called chat.
In order to enable such communication, I use SignalR library. I create connection at first visit to my page (main page). So JS code, which creates connection, creates variable used to configure connection.
Then user enters chat room, which is different page, so new JSs are loaded etc. The one, which held connection variable is now unavailable. But now I need that connection to send messages in my chat room.
So this variable must be "transfered" over to next scripts.
So, what would be the way to actually persist connection through whole session on the website?
Finally I got my answer.
It was suggested that I should use ASP NET Identity, which is very valid, but I already created simple authentication and users management. It isn't safe and as half good as ASP NET Identity (I looked throught it and got my head around it), but it's just personal project, not production one, so if it evolves I might switch to Identity or even implement one myself ;) But it's not the case.
It required little bit of extra steps, so:
I needed to enable sessions in ASP.NET Core, I used this article for this purpose. Having that, I can persist my user login and provide it to user ID provider for signalR
Adding cutsom user ID provider for SignalR in .NET:
I needed to create such class
public class UserIdProvider : IUserIdProvider
{
public static readonly string SESSION_LOGIN_KEY = "loggedUser";
private readonly IHttpContextAccessor _httpContextAccessor;
public UserIdProvider(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public string GetUserId(HubConnectionContext connection)
{
var session = _httpContextAccessor.HttpContext.Session;
session.TryGetValue(SESSION_LOGIN_KEY, out byte[] loginBA);
if (loginBA == null)
{
return null;
}
return new string(loginBA.Select(b => (char)b).ToArray());
}
}
So, after logging I can set login in Session, so it becomes "state" variable, and use it in above class.
Also, one need to add it in ASP services, like below (in Startup.ConfigureServices):
services.AddSingleton<IUserIdProvider, UserIdProvider>();
There also one thing, which still need to be set: in UserIdProvider we need to access Session through HttpContext. In order to use HttpContextwe need to specify it like below (also in Startup.ConfigureServices):
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
which passes HttpContextAccessor to serices constructors.
After all this, you can access users in SignalR Hub with their logins, which is set in Context.UserIdnentifier
This also enables sending messages to specific user, just by passing their login (frontend client just chooses user), like below:
public async Task SendMessage(string message, string user)
{
await Clients.User(user).SendAsync("ReceiveMessage", message).ConfigureAwait(false);
}
NOTE There was one problem though. Browsers on computer didn't persist sessions, which I solved by (also in Startup.ConfigureServices):
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => false; // <~~~~ This needs to be set to false
options.MinimumSameSitePolicy = SameSiteMode.None;
});
Without it, you need to be carefull with cookies, if you don't accept them on a site, it won't work, as user's login won't be persisted.
On the server side, you can send the message to specific user via user id:
var user = await _userManager.GetUserAsync(Context.User);
await Clients.User(user.Id).SendCoreAsync("msg", new object[] { user.Id, user.Email });
On the client side, whenever the connection is started and the hub msg is listened, the user will receive the message:
connection.on('msg', function (...data) {
console.log('data:', data);
});
By sending message(s) via user id, you don't need to care where the target user is.
public class ChatHub : Hub
{
private IUserManager<ApplicationUser> _userManager;
public ChatHub(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
public async Task GetInfo()
{
var user = await _userManager.GetUserAsync(Context.User);
await Clients.User(user.Id).SendCoreAsync("msg", new object[] { user.Id, user.Email });
}
}

Problem returning view MVC/Javascript/.NET Core

I'm fairly new to MVC, and I seem to be having a problem returning a view from JS. I'm working on a chatting site, and this code creates a new chatroom with the name you assign it. Here's my JS code:
function CreateConversation() {
var conversationName = document.getElementById("conversationName").value;
var username = document.getElementById("usernameLabel").textContent;
window.location.href = "/CreateNewChat/CreateConversation?username=" + username + "&convName=" + conversationName;
}
That should call this ActionResult method:
public IActionResult CreateConversation(string username, string convName)
{
ChatModel model = new ChatModel(username, convName);
model.Chatters.Add(username);
return RedirectToAction("Chat","Chat", model);
}
Which does, as long as I don't type any conversation name in the text box. If I check the JS code, both username and conversation name values are correctly set up, but the ActionResult method will not be hit if I fill in the conversation name.
Here's the Chat method in the ChatController:
public IActionResult Chat(ChatModel model)
{
return View(model);
}
If I don't put any conversation name in, it will come all the way to this method, but it won't work (as that field is obligatory and I need it to load the view). Any ideas? I've tried using ajax but while it will pass the conversation name along, it will get stuck at the RedirectToAction statement (will never hit the Chat method in the ChatController). I'm really lost here.
Generally, you shouldn't pass objects around like that - at best you should only pass around ViewModels to prevent someone from fudging data and potentially breaking your app.
The way you have it with your redirect will actually push the whole model into the URL parameters before stepping back into code, leaving it wide open for someone to change as they see fit.
Ideally you would post to create a persisted instance then redirect by passing only an identifier to the Chat() method like so:
CreateNewChatController.cs
public ActionResult CreateConversation(string username, string convname)
{
Guid chatId = yourServiceFactory.CreateNewChat(username, convname);
// You passed the service the username anyway so it can do the Chatters.Add() internally
return RedirectToAction("Chat", "Chat", new { id = chatId });
}
ChatController.cs
public ActionResult Chat(Guid id)
{
ChatModel model = yourServiceFactory.GetChatById(id);
return View(model);
}
However, if you really want to pass the model onto ActionResult Chat(ChatModel model), then you can execute and return the controller action directly.
public IActionResult CreateConversation(string username, string convname)
{
ChatModel model = new ChatModel(username, convname);
model.Chatters.Add(username);
ChatController chatController = new ChatController();
return chatController.Chat(model);
}
I've created a very simplistic working example of this based on what you've said above, so any validation you may have in your ChatModel constructor is omitted in my example.
For any other errors, I would suggest checking to see if the values you're passing in aren't being detected as "potentially harmful" by the app and being dropped.
Noting that the use of .textContent instead of value would get the full contents between the start and ending tags of a given element, I'm assuming the element is a textarea type not a normal text input. You could be getting extra elements between <textarea>...</textarea>
In my example, my javascript function just takes two local variables declared and set within the function. Without seeing your submit page, I can't really comment on why else setting a value would prevent a call from working.

Retrieve values from existing open tab in Chrome using C#

I am trying to build a console application using c# .net.. I need one particular functionality which is to retrieve values from an external website. The thing is we need to sign in to that website. I am able to open the page that i need using process.start in chrome signed in with the values i need.. but problem is when retrieving the value from page.. i thought of getting source code but every way i try it does not take the session and hence i am getting just error page source code as i am just entering the URL n not accessing the already opened tab? Is there any other way available either using JavaScript or c#?
Use WebClient API to login and download page data. You will need to make it cookie aware in order to maintain session. You will need to add reference to System.Web Assembly to use this API in console application.
public class CookieAwareWebClient : WebClient
{
public CookieAwareWebClient()
{
CookieContainer = new CookieContainer();
}
public CookieContainer CookieContainer { get; private set; }
protected override WebRequest GetWebRequest(Uri address)
{
var request = (HttpWebRequest)base.GetWebRequest(address);
request.CookieContainer = CookieContainer;
return request;
}
}
Now use it like
using (var client = new CookieAwareWebClient())
{
var values = new NameValueCollection
{
{ "username", "john" },
{ "password", "secret" },
};
client.UploadValues("http://domain.loc/logon.aspx", values);
// If the previous call succeeded we now have a valid authentication cookie
// so we could download the protected page
string result = client.DownloadString("http://domain.loc/testpage.aspx");
}
I borrowed this piece of code from WebClient accessing page with credentials

Not able to call client method on OnDisconnected

I've a disconnect method in TestHub(which is inherited by Hub) class for signal R. I'm not able to call javascript method fnDeleteCustomer from OnDisconnected method, however same same js method gets called on Connect method. What I'm doing wrong?
public override Task OnDisconnected()
{
try
{
var customer = ConnectedUsers.Find(x => x.ConnectionID == Context.ConnectionId);
if (customer!=null)
{
Clients.Client(customer.ConnectionID).fnDeleteCustomer(customer.UserId);
return base.OnDisconnected();
}
}
catch { };
return null;
}
According to MSDN:
Occurs when a connection disconnects from this hub instance.
So you have no any alive connection and you cannot access the client hub and its methods.
I suppose you should use client-side disconnected event:
$.connection.hub.disconnected(function() {
$.connection.hub.fnDeleteCustomer(userId);
});
More information about signalr lifetime events can be found here.
You are not able to execute fnDeleteCustomer because at the moment of executing OnDisconnected, the client had already disconnected from the hub, so in that moment, the client wil not have a ConnectionId.
Sure, you can use the client disconnected method, but the SignalR disconnection mostly happens when the client leaves the respective page.
From my point of view, it is not the client who just left that you want to execute the fnDeleteCustomer method, but the remaining ones so they can be notified that somebody left.
Hope this helps! Best of luck!
EDIT:
If you want to notify all the other clients that someone left, you simply do so:
public override OnDisconnected()
{
var customer = ConnectedUsers.Find(x => x.ConnectionID == Context.ConnectionId);
Clients.All.notifySomeoneLeft(customer.Name);
}
Then, you create your client method notifySomeoneLeft:
$.connection.client.notifySomeoneLeft = function(customerName){
alert(customerName + "just left!");
};
And that's it. Everytime someone leaves, all the connected clients will get an alert.
Best of luck!
public override OnDisconnected()
{
var customer = ConnectedUsers.Find(x => x.ConnectionID == Context.ConnectionId);
Clients.All.notifySomeoneLeft(customer.Name);
}
The context.connectionId is getting new connectionId instead of old one.

Categories

Resources