I am implementing a multi-tenant application using cosmosDB. I am using partition keys to separate multiple users data. Following best practices i am trying to allow each tenant to have its own db access token.
I create a user and permission and use the created token to access the partition. But I get the following error:
Partition key provided either doesn't correspond to definition in the collection or doesn't match partition key field values specified
in the document.
ActivityId: 1659037a-118a-4a2d-8615-bb807b717fa7, Microsoft.Azure.Documents.Common/1.22.0.0, Windows/10.0.17134
documentdb-netcore-sdk/1.9.1
My code goes as follows:
Constructor Initiates the client
public Projects (CosmosDbConfig cosmosConfig)
{
config = cosmosConfig;
client = new DocumentClient(new Uri(config.Endpoint), config.AuthKey);
collectionUri = UriFactory.CreateDocumentCollectionUri(config.Database, config.Collection);
config.AuthKey = GetUserToken().Result;;
client = new DocumentClient(new Uri(config.Endpoint), config.AuthKey);
}
The get user function creates the user and retrieves the token. User Ids are partition keys.
private async Task<string> GetUserToken()
{
User user = null;
try
{
try
{
user = await client.ReadUserAsync(UriFactory.CreateUserUri(config.Database, config.PartitionKey));
var permission = await GetorCreatePermission(user, config.Collection, config.PartitionKey);
return permission.Token;
}
catch (Exception ex) {
Console.WriteLine(ex.Message);
}
if (user == null)
{
user = new User
{
Id = config.PartitionKey
};
user = await client.CreateUserAsync(UriFactory.CreateDatabaseUri(config.Database), user);
var permission = await GetorCreatePermission(user, config.Collection, config.PartitionKey);
return permission.Token;
}
else
{
throw new Exception("");
}
}
catch (Exception ex)
{
throw ex;
}
}
Permission are done per collections and holds the collection name as ID since Ids are unique per user.
private async Task<Permission> GetorCreatePermission(User user,
string collection,
string paritionKey)
{
var permDefinition = new Permission
{
Id = collection,
PermissionMode = PermissionMode.All,
ResourceLink = collectionUri.OriginalString,
ResourcePartitionKey = new PartitionKey(paritionKey),
};
var perms = client.CreatePermissionQuery(user.PermissionsLink).AsEnumerable().ToList();
var perm = perms.FirstOrDefault(x => x.Id == collection);
if (perm != null)
{
return perm;
}
else
{
var result = await client.CreatePermissionAsync(user.SelfLink, permDefinition);
perm = result.Resource;
return perm;
}
}
The create function utilizes the new client and this where the error occurs.
public async Task<string> Create(Project p)
{
var result = await client.CreateDocumentAsync(collectionUri, p, new RequestOptions()
{ PartitionKey = new PartitionKey(config.PartitionKey),
});
var document = result.Resource;
return document.Id;
}
Since error says that partition key is incorrect i can suggest you try define partition key pathes while creating collection:
var docCollection = new DocumentCollection();
docCollection.Id = config.CollectionName;
docCollection.PartitionKey.Paths.Add(string.Format("/{0}", config.PartitionKey );
collectionUri = UriFactory.CreateDocumentCollectionUri(config.Database, docCollection);
I'm currently having a problem with optimistic concurrency in my ASP.NET MVC application.
Essentially, this following controller works fine when called upon once. However, I have a page that is made to modify residents in six different groups at the same time. This is where the problem occurs. Basically, I post up the list of residents one at a time for each list so there are essentially six concurrent ajax calls that hit the server at the same time. (This may be what I need to change, but I am not sure!)
The javascript ajax call posts up the id of the group along with a list of the resident IDs. The residents are then added to the groups references. I have been looking into refreshing the state, but this isn't working as it seems only one of the six groups is updated. I have tried also letting the client win. I really just need some guidance and tips on resolving this issue!
Here is my current code in my controller:
var group = _context.TherapyGroups.Include(r => r.Residents)
.Where(x => x.ID.ToString() == groupid).FirstOrDefault();
if(group == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
group.Residents.Clear();
foreach(var id in residents)
{
var resident = _context.Residents.Where(x => x.ID.ToString() == id).FirstOrDefault();
if(resident == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
group.Residents.Add(resident);
}
bool saveFailed;
do
{
saveFailed = false;
try
{
_context.SaveChanges();
}
catch (DbUpdateException e)
{
saveFailed = true;
((IObjectContextAdapter)_context).ObjectContext.Refresh(RefreshMode.StoreWins, _context.Residents);
}
} while (saveFailed);
Wasn't sure how to fix the issue you are having with the code you have, so tried simplifying it a bit. See if this helps:
var groupId = _context.TherapyGroups.FirstOrDefault(x => x.ID.ToString() == groupid).Select(x => x.ID);
if(groupId == "") // or if(groupId == 0), not sure type of x.ID
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
// Get all of the residents whose Id is in the residents list
var residentObjects = _context.Residents.Where(x => residents.Contains( x.ID.ToString()));
// Update the groupId for each resident
foreach(var resident in residentObjects)
{
resident.GroupId = groupId;
}
var saveFailed = false;
try
{
_context.SaveChanges();
}
catch (DbUpdateException e)
{
saveFailed = true;
}
public bool IsUserGroupMember(string user, string unit)
{
bool member = false;
try
{
PrincipalContext ctx = new PrincipalContext(ContextType.Domain);
string[] groups = unit.Split(',');
foreach (string word in groups)
{
GroupPrincipal grp = GroupPrincipal.FindByIdentity(ctx, IdentityType.Name, word);
if (grp != null)
{
foreach (Principal p in grp.GetMembers(true))
{
if (p.SamAccountName == user)
{
member = true;
grp.Dispose();
ctx.Dispose();
return member;
}
}
}
else
{
grp.Dispose();
ctx.Dispose();
return member;
}
}
}
catch (COMException)
{
return member;
}
return member;
}
I'm using the method above to find if a user is member of a group in Active Directory, recursively. It works well..although sometimes I get a weird exception.
Specified method is not supported.
foreach (Principal p in grp.GetMembers(true)) is red (sorry I can't upload a picture of the exception). The weirdest thing is that it seems to be thrown randomly, and if I refresh the page it works well..
I tried to find a solution on the Internet but no happy news for now..
You should do this the other way around: get the user and then the authorization groups that this user is a member of - this call (.GetAuthorizationGroups on the UserPrincipal) already is searching the groups recursively for you!
public bool IsUserGroupMember(string user, string unit)
{
bool isMember = false;
try
{
// put the PrincipalContext in a using(..) block - then it's
// automatically, safely and properly disposed of at the end
using (PrincipalContext ctx = new PrincipalContext(ContextType.Domain))
{
// get the user
UserPrincipal up = UserPrincipal.FindByIdentity(ctx, user);
if(up != null)
{
// get the authorization groups for the user
// this call is *RECURSIVELY* enumerating all groups
// that this user is a member of
var authGroups = up.GetAuthorizationGroups();
// now that you have the groups - just determine if the user
// is a member of the group you're looking for......
}
}
}
catch (COMException comEx)
{
isMember = false;
}
return isMember;
}
I finally ended up with a solution!
I just had to add my domain name, as follow:
PrincipalContext ctx = new PrincipalContext(ContextType.Domain, "MyDomain");
It solved the problem right away!
And about the slowness..I used cookies, following this link.
I'm having some trouble with Sqlite in memory.
I have a class that has a CPF field - similar to US' SSN. As a bussiness rule, the CPF must be unique in the system.
So I've decided to make a check on the class that has this field. Now maybe there's code smell here: I check with the ORM if this is a Conflicting CPF.
private CPF cpf;
public virtual CPF CPF
{
get { return cpf; }
set
{
if (this.ormCreated) //Do not check if it is loaded from the DB. Otherwise, it loops, generating a StackOverflow exception
{
cpf = value;
}
else
{
this.setNewCpf(value);
}
}
}
private void setNewCpf(CPF newCpf)
{
if (this.cpf == newCpf)
{
return;
}
if (Helper.Orm.IsConflictingCpf(newCpf))
{
throw new ConflictingCpfException();
}
else
{
cpf = newCpf;
}
}
And here is the implementation, on the ORM Helper class.
bool OrmHelper.IsConflictingCpf(CPF cpf)
{
int? cpfNumber = cpf.NumeroSemDV;
if (cpfNumber.HasValue)
{
var teste = findByCpfNumber<Client>(cpf);
return
(
findByCpfNumber<Client>(cpf) != null ||
findByCpfNumber<Adversary>(cpf) != null
);
}
else
{
//CPFSemDV = Nullable
return false;
}
}
private PersonType findByCpfNumber<PersonType> (CPF cpf) where PersonType : PessoaFisica
{
int? cpfNumber = cpf.NumeroSemDV;
using (var session = this.NewSession())
using (var transaction = session.BeginTransaction())
{
try
{
var person = session.Query<PersonType>()
.Where(c => c.CPF.NumeroSemDV == cpfNumber)
.FirstOrDefault<PersonType>();
return person;
}
catch (Exception) { transaction.Rollback(); }
finally
{
session.Close();
}
}
return null;
}
The problem happens in my tests. I'm using FluentNHibernate and In memory SQLite.
protected override FluentConfiguration PersistenceProvider
{
get
{
return Fluently
.Configure()
.Database(
SQLiteConfiguration
.Standard
.InMemory()
.ShowSql()
);
}
}
Here is the failing test.
protected override void Given()
{
base.Given();
var clients = new List<Client>();
Client client1 = new Client("Luiz Angelo Heinzen")
{
Capaz = true,
CPF = new CPF(18743509),
eMail = "lah#furb.br"
};
session.Save(client1);
session.Evict(client1);
}
[Then]
public void Motherfaker()
{
Client fromDb;
var clientsFromDb = session.Query<Client>()
.Where(c => c.eMail == "lah#furb.br");
fromDb = clientsFromDb.FirstOrDefault<Client>();
Assert.AreEqual(fromDb.FullName, "Luiz Angelo Heinzen");
}
The reason it fails? In the beginning it was failing because the table didn't exist. In memory sqlite destroys the schema on each new session. So I changed the code to return the same session on the NewSession(). But now it fails with a NHibernate exception: Session is closed. I've tested and if change the findByCpfNumber from this
private PersonType findByCpfNumber<PersonType> (CPF cpf) where PersonType : PessoaFisica
{
int? cpfNumber = cpf.NumeroSemDV;
using (var session = this.NewSession())
using (var transaction = session.BeginTransaction())
{
try
{
var person = session.Query<PersonType>()
.Where(c => c.CPF.NumeroSemDV == cpfNumber)
.FirstOrDefault<PersonType>();
return person;
}
catch (Exception) { transaction.Rollback(); }
finally
{
session.Close();
}
}
return null;
}
to this
private PersonType findByCpfNumber<PersonType> (CPF cpf) where PersonType : PessoaFisica
{
int? cpfNumber = cpf.NumeroSemDV;
//using (var session = this.NewSession())
var session = this.NewSession();
using (var transaction = session.BeginTransaction())
{
try
{
var person = session.Query<PersonType>()
.Where(c => c.CPF.NumeroSemDV == cpfNumber)
.FirstOrDefault<PersonType>();
return person;
}
catch (Exception) { transaction.Rollback(); }
finally
{
//session.Close();
this.CloseSession(session);
}
}
this.CloseSession(session);
return null;
}
the error doesn't happen anymore. Obviously, I'd have to implement the CloseSession method. It would close the Session on the Production database and it would do nothing if Sqlite is being used.
But I'd rather configure SQLite in someway that it wouldn't dispose the session. I've read here about release_mode, Pooling and Max Pool atributes. But I can't seem to find it in the FluentNHibernate so can't even test to see if it would work. I have the FluentNHibernate cloned and it seems to set the release_mode set to on_close, but that doesn't help.
I've even tried:
public override ISession NewSession()
{
if (this.session == null)
{
if (sessionFactory == null)
{
CreateSessionFactory();
}
this.session = sessionFactory.OpenSession();
}
if (!session.IsOpen)
{
sessionFactory.OpenSession(session.Connection);
session.Connection.Open();
}
return session;
}
But it keeps telling me that the Session is closed. So, anyone has any suggestions on how to approach this?
Or does this so smelly that's beyond salvation?
I hope this is clear enough. And forgive my mistakes: I'm from Brazil and not a native english speaker.
Thanks,
Luiz Angelo.
i would check for uniqueness when creating CPFs in the system and have an additional Unique constraint in the database to enforce that. Then if you set cascading to none for each reference to CPF (default is none) it is not possible to assigne newly created duplicate CPFs to an Entity and save it without exception, so it can't happen accidently.
I had the same problem. What's happening is that in-memory SQLite will drop the entire schema when the connection is closed. If you create a session that you hold on to for all tests, it will retain the structure for all other sessions.
For code and a fuller explanation, check out this answer: Random error when testing with NHibernate on an in-Memory SQLite db
I have a page in my application which always shows updated list of online users.
Now, to keep the list-which is stored in application object- updated, i do the below steps
add user to list when login
remove user on log off
Then to handle browser close/navigate away situations, I have a timestamp along with the username in the collection
An ajax call every 90 seconds updates the timestamp.
The problem:
I need something to clean this list every 120 seconds to remove entries with old timestamps.
How do I do this within my web application? ie Call a function every 2 mins.
PS: I thought of calling a webservice every 2 mins using a scheduler , but the hosting environment do not allow any scheduling.
Do the following inside a global filter.
public class TrackLoginsFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
Dictionary<string, DateTime> loggedInUsers = SecurityHelper.GetLoggedInUsers();
if (HttpContext.Current.User.Identity.IsAuthenticated )
{
if (loggedInUsers.ContainsKey(HttpContext.Current.User.Identity.Name))
{
loggedInUsers[HttpContext.Current.User.Identity.Name] = System.DateTime.Now;
}
else
{
loggedInUsers.Add(HttpContext.Current.User.Identity.Name, System.DateTime.Now);
}
}
// remove users where time exceeds session timeout
var keys = loggedInUsers.Where(u => DateTime.Now.Subtract(u.Value).Minutes >
HttpContext.Current.Session.Timeout).Select(u => u.Key);
foreach (var key in keys)
{
loggedInUsers.Remove(key);
}
}
}
To retrieve the user list
public static class SecurityHelper
{
public static Dictionary<string, DateTime> GetLoggedInUsers()
{
Dictionary<string, DateTime> loggedInUsers = new Dictionary<string, DateTime>();
if (HttpContext.Current != null)
{
loggedInUsers = (Dictionary<string, DateTime>)HttpContext.Current.Application["loggedinusers"];
if (loggedInUsers == null)
{
loggedInUsers = new Dictionary<string, DateTime>();
HttpContext.Current.Application["loggedinusers"] = loggedInUsers;
}
}
return loggedInUsers;
}
}
Don't forget to Register you filter in global.asax. It's probably a good idea to have an app setting to switch this off.
GlobalFilters.Filters.Add(new TrackLoginsFilter());
Also remove users at logoff to be more accurate.
SecurityHelper.GetLoggedInUsers().Remove(WebSecurity.CurrentUserName);
In your Account Controller
public ActionResult Login(LoginModel model, string returnUrl)
{
if (ModelState.IsValid)
{
if (Membership.ValidateUser(model.UserName, model.Password))
{
FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
if (HttpRuntime.Cache["LoggedInUsers"] != null) //if the list exists, add this user to it
{
//get the list of logged in users from the cache
List<string> loggedInUsers = (List<string>)HttpRuntime.Cache["LoggedInUsers"];
//add this user to the list
loggedInUsers.Add(model.UserName);
//add the list back into the cache
HttpRuntime.Cache["LoggedInUsers"] = loggedInUsers;
}
else //the list does not exist so create it
{
//create a new list
List<string> loggedInUsers = new List<string>();
//add this user to the list
loggedInUsers.Add(model.UserName);
//add the list into the cache
HttpRuntime.Cache["LoggedInUsers"] = loggedInUsers;
}
if (!String.IsNullOrEmpty(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
else
{
ModelState.AddModelError("", "The user name or password provided is incorrect.");
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
public ActionResult LogOff()
{
string username = User.Identity.Name; //get the users username who is logged in
if (HttpRuntime.Cache["LoggedInUsers"] != null)//check if the list has been created
{
//the list is not null so we retrieve it from the cache
List<string> loggedInUsers = (List<string>)HttpRuntime.Cache["LoggedInUsers"];
if (loggedInUsers.Contains(username))//if the user is in the list
{
//then remove them
loggedInUsers.Remove(username);
}
// else do nothing
}
//else do nothing
FormsAuthentication.SignOut();
return RedirectToAction("Index", "Home");
}
in your partial view.
#if (HttpRuntime.Cache["LoggedInUsers"] != null)
{
List<string> LoggedOnUsers = (List<string>)HttpRuntime.Cache["LoggedInUsers"];
if (LoggedOnUsers.Count > 0)
{
<div class="ChatBox">
<ul>
#foreach (string user in LoggedOnUsers)
{
<li>
<div class="r_row">
<div class="r_name">#Html.Encode(user)</div>
</div>
</li>
}
</ul>
</div>
}
}
render this partial view when user log in.
use this script call ever 90 second
<script type="text/javascript">
$(function () {
setInterval(loginDisplay, 90000);
});
function loginDisplay() {
$.post("/Account/getLoginUser", null, function (data) {
});
}
</script>
Here is the white elephant solution.
Instead of maintaining this list in application object, maintain this list in database. Then you can use database jobs to work on this list periodically. Establish SQL notification on this object so that everytime this list is purged you get refreshed data in your application.
Use Ajax to send "I am still online" message to the server in every 30 seconds. This is the best way to find who is really online.
So here what I did:
Create a table in the database
CREATE TABLE [dbo].[OnlineUser]
(
[ID] [int] IDENTITY(1,1) NOT NULL,
[Guid] [uniqueidentifier] NOT NULL,
[Email] [nvarchar](500) NOT NULL,
[Created] [datetime] NOT NULL,
CONSTRAINT [PK_OnlineUser] PRIMARY KEY CLUSTERED
(
[ID] ASC
) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
Override the OnActionExecution method. This method is in a separate controller in my case is called AuthController then every other controller that required authemtication inherits from this controller.
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
// session variable that is set when the user authenticates in the Login method
var accessSession = Session[Constants.USER_SESSION];
// load cookie is set when the user authenticates in the Login method
HttpCookie accessCookie = System.Web.HttpContext.Current.Request.Cookies[Constants.USER_COOKIE];
// create session from cookie
if (accessSession == null)
{
if (accessCookie != null)
{
if (!string.IsNullOrEmpty(accessCookie.Value))
accessSession = CreateSessionFromCookie(accessCookie);
}
}
// if session does not exist send user to login page
if (accessSession == null)
{
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary
{
{"controller", "Account"},
{"action", "Login"}
}
);
return;
}
else
{
TrackLoggedInUser(accessSession.ToString());
}
}
private List<OnlineUser> TrackLoggedInUser(string email)
{
return GetOnlineUsers.Save(email);
}
Next I created the following classes in the Data Repository class: GetOnlineUsers
public static class GetOnlineUsers
{
public static List<OnlineUser> GetAll()
{
using (var db = new CEntities())
{
return db.OnlineUsers.ToList();
}
}
public static OnlineUser Get(string email)
{
using (var db = new CEntities())
{
return db.OnlineUsers.Where(x => x.Email == email).FirstOrDefault();
}
}
public static List<OnlineUser> Save(string email)
{
using (var db = new CEntities())
{
var doesUserExist = db.OnlineUsers.Where(x => x.Email.ToLower() == email.ToLower()).FirstOrDefault();
if (doesUserExist != null)
{
doesUserExist.Created = DateTime.Now;
db.SaveChanges();
}
else
{
OnlineUser newUser = new OnlineUser();
newUser.Guid = Guid.NewGuid();
newUser.Email = email;
newUser.Created = DateTime.Now;
db.OnlineUsers.Add(newUser);
db.SaveChanges();
}
return GetAll();
}
}
public static void Delete(OnlineUser onlineUser)
{
using (var db = new CEntities())
{
var doesUserExist = db.OnlineUsers.Where(x => x.Email.ToLower() == onlineUser.Email.ToLower()).FirstOrDefault();
if (doesUserExist != null)
{
db.OnlineUsers.Remove(doesUserExist);
db.SaveChanges();
}
}
}
}
In the Global.asax
protected void Application_EndRequest()
{
// load all active users
var loggedInUsers = GetOnlineUsers.GetAll();
// read cookie
if (Context.Request.Cookies[Constants.USER_SESSION] != null)
{
// the cookie has the email
string email = Context.Request.Cookies[Constants.USER_SESSION].ToString();
// send the user's email to the save method in the repository
// notice in the save methos it also updates the time if the user already exist
loggedInUsers = GetOnlineUsers.Save(email);
}
// lets see we want to clear the list for inactive users
if (loggedInUsers != null)
{
foreach (var user in loggedInUsers)
{
// I am giving the user 10 minutes to interact with the site.
// if the user interaction date and time is greater than 10 minutes, removing the user from the list of active user
if (user.Created < DateTime.Now.AddMinutes(-10))
{
GetOnlineUsers.Delete(user);
}
}
}
}
In one of the controllers (You can create a new one up to you) that inhering from the AuthController, create the following method:
public JsonResult GetLastLoggedInUserDate()
{
string email = Session[Constants.USER_SESSION].ToString();
var user = GetOnlineUsers.Get(email);
return Json(new { year = user.Created.Year,
month = user.Created.Month,
day = user.Created.Day,
hours = user.Created.Hour,
minutes = user.Created.Minute,
seconds = user.Created.Second,
milliseconds = user.Created.Millisecond
}, JsonRequestBehavior.AllowGet);
}
In your _Layout.cshtml file at the very bottom place this Javascript code: This Javascript code will call the GetLastLoggedInUserDate() above to get the last interacted date from the database.
<script>
var lastInteracted, DifferenceInMinutes;
$(window).on('load', function (event) {
$.get("get-last-interaction-date", function (data, status) {
lastInteracted = new Date(data.year.toString() + "/" + data.month.toString() + "/" + data.day.toString() + " " + data.hours.toString() + ":" + data.minutes.toString() + ":" + data.seconds.toString());
});
});
$(window).on('mousemove', function (event) {
var now = new Date();
DifferenceInMinutes = (now.getTime() - lastInteracted.getTime()) / 60000;
if (DifferenceInMinutes > 5) {
$.get("get-last-interaction-date", function (data, status) {
lastInteracted = new Date(data.year.toString() + "/" + data.month.toString() + "/" + data.day.toString() + " " + data.hours.toString() + ":" + data.minutes.toString() + ":" + data.seconds.toString());
});
}
});
</script>
JavaScript explanation:
On page load I am are setting the last datetime the the user interacted with my website.
Since I cannot track what the user stares at on the screen, the next closest thing to real interaction is mouse movement.
So when the user moves the mouse anywhere on the page the following happens:
I compare the last interacted date with the current date.
Then I check if 5 minutes passed since the last updated date occurred.
Since the user happened to love the website and decided to spend more time on it, after the 5 minutes are passed, I send another request to the this method in my controller GetLastLoggedInUserDate() to get the date again. But before we get the date we will execute the OnActionExecuting method which will then update the records Created date and will return the current time. The lastInteracted gets the updated date and we go again.
The idea here is that when the user is not interacting with my website he is not really online for me. Maybe he has 100 tabs open and playing games doing other things but interacting with my website it is possible that they will not even realize they have it open in days or months depends on how often they reboot the PC. In any case I think that 10 minutes is a good threshold to work with but feel free to change it.
Finally AdminController class:
public ActionResult Index()
{
DashboardViewModel model = new DashboardViewModel();
// loading the list of online users to the dashboard
model.LoggedInUsers = GetOnlineUsers.GetAll();
return View("Index", "~/Views/Shared/_adminLayout.cshtml", model);
}
Index.cshtml (admin dashboard page)
#model ILOJC.Models.Admin.DashboardViewModel
#{
ViewBag.Menu1 = "Dashboard";
}
/// some html element and styles
<h5 class="">#Model.LoggedInUsers.Count() Online Users</h5>
<div class="row">
#foreach (var user in Model.LoggedInUsers.OrderByDescending(x => x.Created))
{
<div class="col-md-12">
<h5>#user.Email</h5>
<p><span>Last Inreaction Time: #user.Created.ToString("MM/dd/yyyy hh:mm:ss tt")</span></p>
</div>
}
</div>
Since the original table will only store online users I wanted to have a bit of history/log so I create a history table in the database:
CREATE TABLE [dbo].[OnlineUserHistory](
[ID] [int] IDENTITY(1,1) NOT NULL,
[OnlineUserID] [int] NOT NULL,
[Guid] [uniqueidentifier] NOT NULL,
[Email] [nvarchar](500) NOT NULL,
[Created] [datetime] NOT NULL,
[Updated] [datetime] NOT NULL,
[Operation] [char](3) NOT NULL,
CONSTRAINT [PK_OnlineUserLog] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
Lastly, I created a database Trigger on insert and delete
CREATE TRIGGER [dbo].[trg_online_user_history]
ON [dbo].[OnlineUser]
AFTER INSERT, DELETE
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO OnlineUserHistory(
OnlineUserID,
[Guid],
Email,
Created,
Updated,
Operation
)
SELECT
i.ID,
i.[Guid],
i.Email,
i.Created,
GETDATE(),
'INS'
FROM
inserted i
UNION ALL
SELECT
d.ID,
d.[Guid],
d.Email,
d.Created,
GETDATE(),
'DEL'
FROM
deleted d;
END
Hope this can hep someone. One thing I would improve tho is the way the online users are displaying load in the dashboard. Now, I need to refresh the page to see the updated number. But if you want to see it live, you just add the SignalR library then create a hub and you good to go!