Blazor + SQLTableDependency + SignalR: Notify specific groups from OnChange event - c#

I have a Blazor application that uses SQLTableDependency to detect database changes, then notify all clients about the change via SignalR. This works but I need a way to be able to detect changes and only notify specific SignalR groups. Because SQLTableDependency doesn't care about who inserted, altered, or deleted a record in the database, I am not sure how to know which group to send the update too. Please see below for more details about my app and what i'm trying to accomplish.
For each customer we setup a new organization. An organization has its own list of assets, and can have multiple users.
Organization.cs
public class Organization
{
public int OrganizationId { get; set; }
public string OrganizationName { get; set; }
public List<Asset> Assets { get; set; }
public List<ApplicationUser> Users { get; set; }
public bool IsDisabled { get; set; }
}
Asset.cs
public class Asset
{
public int AssetId { get; set; }
public string SerialNumber { get; set; }
public int OrganizationId { get; set; }
public virtual Organization Organization { get; set; }
public DateTime DateAdded { get; set; }
}
ApplicationUser.cs
public class ApplicationUser
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int OrganizationId { get; set; }
public virtual Organization Organization { get; set; }
public List<Connection> Connections { get; set; }
public string Timezone { get; set; }
}
Connection.cs - I am storing each SignalR Connection in the database.
public class Connection
{
public string ConnectionId { get; set; }
public string UserName { get; set; }
public bool Connected { get; set; }
public string Group { get; set; }
public DateTime ConnectionTime { get; set; }
}
AssetService.cs
public class AssetService : IAssetService
{
private readonly IServiceScopeFactory _serviceScopeFactory;
public AssetService(IServiceScopeFactory serviceScopeFactory)
{
_serviceScopeFactory = serviceScopeFactory;
}
public async Task<Asset> AddAssetAsync(Asset asset, string currentUserName)
{
try
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var db = scope.ServiceProvider.GetService<DataContext>();
if (asset.Device != null)
{
db.Entry(asset.Device).State = EntityState.Modified;
}
asset.DateAdded = DateTime.UtcNow;
await db.Assets.AddAsync(asset);
await db.SaveChangesAsync();
return asset;
}
}
catch (System.Exception ex)
{
throw ex;
}
}
}
AssetHub.cs - SignalR Hub
public class ChatHub : Hub
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly IServiceScopeFactory _serviceScopeFactory;
public ChatHub(UserManager<ApplicationUser> userManager, IServiceScopeFactory serviceScopeFactory)
{
_userManager = userManager;
_serviceScopeFactory = serviceScopeFactory;
}
public async Task SendAssetToGroup(string userName, string location, Asset asset)
{
if (!string.IsNullOrWhiteSpace(userName))
{
var user = await _userManager.Users.Include(x => x.Connections).SingleAsync(x => x.UserName == userName);
if (user != null)
{
var group = $"{user.AccountId}-{location}";
await Clients.Group(group).SendAsync("AssetUpdate", user.Email, asset);
}
}
}
public override async Task OnConnectedAsync()
{
var httpContext = Context.GetHttpContext();
var location = httpContext.Request.Query["location"];
using (var scope = _serviceScopeFactory.CreateScope())
{
var db = scope.ServiceProvider.GetService<ApplicationDbContext>();
if (!string.IsNullOrWhiteSpace(userName))
{
var user = await db.Users.Include(x => x.Connections).SingleAsync(x => x.UserName == httpContext.User.Identity.Name);
if (user != null)
{
var group = $"{user.OrganizationId}-{location}";
var connection = new Connection { Connected = true, ConnectionId = Context.ConnectionId, Group = group, UserName = user.UserName };
await Groups.AddToGroupAsync(connection.ConnectionId, group);
user.Connections.Add(connection);
db.Users.Update(user);
}
}
await db.SaveChangesAsync();
}
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
if (!string.IsNullOrWhiteSpace(Context.ConnectionId))
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var db = scope.ServiceProvider.GetService<ApplicationDbContext>();
var connection = await db.Connections.Where(x => x.ConnectionId ==
Context.ConnectionId).FirstOrDefaultAsync();
if (connection != null)
{
await Groups.RemoveFromGroupAsync(connection.ConnectionId, connection.Group);
db.Connections.Remove(connection);
await db.SaveChangesAsync();
}
}
}
await base.OnDisconnectedAsync(exception);
}
}
AssetTableChangeService.cs - Here is where I need help. When SQLTableDependency detects a change to the Assets table, I need to be able to call the SendAssetToGroup Method in the AssetHub. Since users are apart of organizations, I don't want to push the update out to all organizations, I only want to send the update only to users that are apart of the specific organization group.
public class AssetTableChangeService : IAssetTableChangeService
{
private const string TableName = "Assets";
private SqlTableDependency<Asset> _notifier;
private IConfiguration _configuration;
public event AssetChangeDelegate OnAssetChanged;
public StockTableChangeService(IConfiguration configuration)
{
_configuration = configuration;
// SqlTableDependency will trigger an event
// for any record change on monitored table
_notifier = new SqlTableDependency<Asset>(
_configuration.GetConnectionString("DefaultConnection"),
TableName);
_notifier.OnChanged += AssetChanged;
_notifier.Start();
}
private void AssetChanged(object sender, RecordChangedEventArgs<Asset> e)
{
OnAssetChanged.Invoke(this, new AssetChangeEventArgs(e.Entity, e.EntityOldValues));
}
public void Dispose()
{
_notifier.Stop();
_notifier.Dispose();
}
So the flow should look like this.
The user logs in - establishing a connection through SignalR
The connection information is stored in the database.
The connection is added to a SignalR group based on what page the user is connecting from and the OrganizationId.
User creates a new Asset from the UI.
The AddAsset method is called in the Asset Service.
The Asset gets inserted into the database.
The SQLTableDependency detects the change and then calls the AssetChanged handler method.
The AssetChanged handler method calls the OnAssetChanged event.
The AssetHub needs to subscribe to the OnAssetChanged event.
When the OnAssetChanged event is fired, a handler method in the AssetHub needs to call the SendAssetToGroup Method.
When user navigates from the Assets page to another, the SignalR connection is removed from the database, and the connection is removed from the group.
I have everything working up until steps 9 and 10. Is there anyway to make this possible since SQLTableDependency doesn't care about who made the change, so I have no way of looking up the connection group in which the update needs to be pushed too. Any ideas?

When the UI is working with a class for example called : Student
The UI component joins a group called "Student" or "BlahNamespace.Student".
If its a list just the name, if its an entity join the both the name and another group with the ID as a string concatenated
"BlahNamespace.Student:201" In your case you could append the the organization's name as well for finer grain if the db knows this from the entity.
The server can notify the groups as needed depending on the operation.
I inject the hub into the API controller to achieve this.
Personally I would not use the signalr service to transfer the data, keep it light weight, just "signal" the change. The client can then decide how to deal with that. That way the data can be only accessed one way via the API with all the configured security.

Related

How to correctly pass a string id to API controller in Xamarin.Forms

I'm writing a mobile app using Xamarin Forms where I am going to consume an REST API.
At the moment, I have a user model
{
public string UserId { get; set; }
public string UserDisplayName { get; set; }
public int UserRoleId { get; set; }
public string UserFirstName { get; set; }
public string UserLastName { get; set; }
public string UserEmail { get; set; }
public string UserPostcode { get; set; }
public DateTime UserCreatedAt { get; set; }
public DateTime UserModifiedAt { get; set; }
public bool UserDeletedAt { get; set; }
}
And I have defined a GetUser method on my controller
// GET: api/Users/5
[HttpGet("{id}")]
public async Task<ActionResult<User>> GetUser(string id)
{
var user = await _context.User.FindAsync(id);
if (user == null)
{
return NotFound();
}
return user;
}
If I test the API using Postman and parse the string id without quotes(edit) on the route, it works fine. E.g. https://localhost:5051/api/Users/Example. However, if I parse the id within qutoes(edit) it doesn't work: https://localhost:5051/api/Users/"Example"
My problem is, on my mobile client, when it calls the web service that calls the API, it needs to parse a string, which goes with the quotes(edit)- matching the second example.
Does any of you know a solution or a workaround for this?
Thanks in advance
EDIT:
My service method is as follows
public static async Task<IEnumerable<User>> GetUserById(string id)
{
var json = await client.GetStringAsync($"api/users/{id}");
var users = JsonConvert.DeserializeObject<IEnumerable<User>>(json);
return users;
}
And my service call is
var users = await UserService.GetUserById("Example");
EDIT2: Fixed
Service method changed to
public static async Task<User> GetUserById(string id)
{
var json = await client.GetStringAsync($"api/users/{id}");
var users = JsonConvert.DeserializeObject<User>(json);
return users;
}
It turns out the issue was caused by the IEnumerable type on the task definition, which makes sense since I was trying to retrieve a single instance.
Service method changed to
public static async Task<User> GetUserById(string id)
{
var json = await client.GetStringAsync($"api/users/{id}");
var users = JsonConvert.DeserializeObject<User>(json);
return users;
}

ASP.NET MVC ObjectDisposedException when trying to pass id

What I am trying to do is create a function that will pass through the id of the user and then will load up a page that displays all the applications that a user has made to each driving instructor.
I have already made a function that will display all applications from the database regardless of who created the application. I have also made a function that allows users to create applications. I have also created a field in the application table named User_UserId so when an application is created and commited to a database, it will add the specific userId that created the application.
My question is, how would I make it so that when you clicked on show all applications on a specific users account, it would only pull up applications that have that User_UserId attached. Could I add UserId to the class that stores the input form and set the UserId in there to the UserId that is creating the application?
My attempts so far all end up with this error in the User Controller on line - IList<Application> applications = user.Applications.ToList();:
An exception of type 'System.ObjectDisposedException' occurred in EntityFramework.dll but was not handled in user code
The ObjectContext instance has been disposed and can no longer be used for operations that require a connection.
I have a function that allows me to view applications by Driving instructors and that works fine, it uses a similar method where it asks for the specific driving instructor in the create form. Then I have a function that lists the application by driving instructor.
Here is the code. Some main points to make are;
Primary key UserId in user class is set as a string
In the class that stores the input data, there is no UserId field as that is passed in the method.
The field in Applications table that stores the user Id is named User_UserId
Please inform me if there is any more pieces of code you require.
User Controller that opens the view for GetApplications:
public ActionResult GetApplications(string id)
{
User user = userService.GetUser(id);
IList<Application> applications = user.Applications.ToList();
return View(applications);
}
IDAO/DAO Files:
IList<Application> GetApplications(string id, XContext context);
public IList<Application> GetApplications(string id, XContext context)
{
User user = context.Users.Find(id);
return user.Applications.ToList();
}
IService/Service Files:
IList<Application> GetApplications(string id);
public IList<Application> GetApplications(string id)
{
using (var context = new XContext())
{
IList<Application> Applications;
Applications = userDAO.GetApplications(id, context);
return Applications;
}
}
HTML view for GetUser that leads to GetApplications, Model uses User class:
#Html.ActionLink("Show Applications", "GetApplications", "User") ||
Context class:
public class XContext : DbContext
{
public XContext() : base("XContext")
{
Database.SetInitializer(new XInitializer());
}
public DbSet<User> Users { get; set; }
public DbSet<Application> Applications { get; set; }
public DbSet<Instructor> Instructors { get; set; }
}
UserService GetUser function:
public User GetUser(string id)
{
using (var context = new XContext())
{
return userDAO.GetUser(id, context);
}
}
UserDAO GetUser function:
public User GetUser(string id, XContext context)
{
return context.Users.Find(id);
}
User Class:
using System.ComponentModel.DataAnnotations;
public class User
{
[Key]
public string UserId { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public virtual ICollection<Application> Applications { get; set; }
public virtual ICollection<Instructor> Instructors { get; set; }
}
Application Class:
public class Application
{
[Key]
public int ApplicationId { get; set; }
public string Level { get; set; }
public string Manual { get; set; }
public string InstructorName { get; set; }
}
I have set breakpoints on the controller function that breaks and here are the results:
User user = userService.GetUser(id);
id = "1"
user = null
IList<Application> applications = user.Applications.ToList();
id = "1"
applications = null
When I continue and get the error thrown, I am given
id = "1"
applications = null
user.Application = null
Your Application class is incorrect. It doesn't have UserId
public class Application
{
[Key]
public int ApplicationId { get; set; }
public string UserId { get; set; }
public string Level { get; set; }
public string Manual { get; set; }
public string InstructorName { get; set; }
}
And I think you have the same bug in your Instructor class
You have the bugs in your DAO.
Replace
public User GetUser(string id, XContext context)
{
return context.Users.Find(id);
}
with
public User GetUser(string id, XContext context)
{
return context.Users.Include(i=> i.Applications).FirstOrDefault(i=>i.UserId==id);
}
And replace
public IList<Application> GetApplications(string id, XContext context)
{
User user = context.Users.Find(id);
return user.Applications.ToList();
}
with
public IList<Application> GetApplications(string id, XContext context)
{
return context.Applications.Where(i=>i.UserId==id).ToList();
}
User service code:
public List<Application> GetApplications(string id)
{
using (var context = new XContext())
{
return userDAO.GetApplications(id, context);
}
}
Your controller code:
public ActionResult GetApplications(string id)
{
var applications = userService.GetApplications(id);
return View(applications);
}

How to track identity changes in EF Core?

I have a service in my application which creates a User, and saves it to the database. Here's a method of my service:
public async Task<UserDTO> CreateUserAsync(User newUser)
{
var result = (UserDTO)await _userRepository.CreateUserAsync(newUser);
if (result != null)
{
await _userRepository.SaveChangesAsync();
}
return result;
}
And a method from a UserRepository:
public async Task<User> CreateUserAsync(User newUser) => (await _dbContext.AddAsync(newUser)).Entity;
Here's a User class:
public class User
{
public int UserId { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
}
The problem is when the user is being added via service, UserId is not known yet. It has default value 0, then ef core saves it to a database, finding a proper UserId. But value returned by my methods has no UserId updated - it is still 0, and i would like to return updated value. How to achieve that in a proper way?
newUser WILL have an Id read from the database.
Your code is casting from User to UserDTO, which is unlikely to work.

Storing user login data in Business logic layer during session

I've got project with dll dependecies like below:
DataAccessLayer(DAL) <- BusinessLogicLayer(BLL) <- MVC Web App(WEB)
The project needs to connect with database by separated SQL logins (Each SQL login means different App user or in other words already existing database users and passwords will be used as app users/logins). So thre's no way to youse default connection.
I'm passing class "Connection" (Class is defined on BLL so DAL can see it but WEB can't) to my Context class to make connection with database like below:
public class WMSContext : DbContext
{
public WMSContext(Connection con)
: base(con.ContextOption)
{
}
public virtual DbSet<Users> Users { get; set; }
public virtual DbSet<Groups> Groups { get; set; }
public virtual DbSet<UsersConfig> UsersConfig { get; set; }
public virtual DbSet<UsersGroup> UsersGroup { get; set; }
public virtual DbSet<UsersGroupConfig> UsersGroupConfig { get; set; }
public virtual DbSet<ShipmentVehicles> ShipmentVehicles { get; set; }
public virtual DbSet<VSHIPMENTS> VSHIPMENTS { get; set; }
public virtual DbSet<VShipmentsDetails> VShipmentsDetails { get; set; }
}
public class Connection
{
public string Login { get; private set; }
public string Password { get; private set; }
public string Server { get; private set; }
public string Database { get; private set; }
public DbContextOptions<WMSContext> ContextOption { get; private set; }
public Connection(string servAddr, string dbName, string login, string pass)
{
Login = login;
Password = pass;
Server = servAddr;
Database = dbName;
string connStr = "Data Source = " + servAddr + "; Initial Catalog = "+ dbName + "; Persist Security Info = True; User ID = "+ login + "; Password = "+ pass+ "";
var optBuild = new DbContextOptionsBuilder<WMSContext>();
optBuild.UseSqlServer(connStr);
ContextOption = optBuild.Options;
}
}
The problem is that the instance of Connection class should be stored somwhere during user session to perform other requests for certain SQL user. My first thought was to assign instance of Connection to Session. Something like below:
Connection = new Connection(){ some login data }
HttpContext.Session.SetString("Login Data", connection);
but in that case i would have to set DAL dependent to WEB and it not seems to be elegant slolution. Sure i can just extract that Connection class to class liblary that will be shared by all projects but i'm curious if there is some way to store data only in BLL and only temporary and delete them when WEB user session ends?
Or maybe there is some other way than using Sessions?
I suggest you create a context object with an interface that you can inject into the DAL. That way the DAL will depend on your interface but not on any web-specific APIs.
For example
interface IDatabaseAuthenticationContext
{
string DatabaseUserName { get; set; }
string DatabasePassword { get; set ; }
}
class AuthenticationContext: IDatabaseAuthenticationContext
{
private readonly HttpContextBase _httpContext;
public AuthenticationContext(HttpContextbase httpContext)
{
_httpContext = httpContext;
}
public string DatabaseUserName
{
get
{
return _httpContext.Session["DatabaseUserName"];
}
set
{
_httpContext.Session["DatabaseUserName"] = value;
}
}
public string DatabasePassword
{
get
{
return _httpContext.Session["DatabasePassword"];
}
set
{
_httpContext.Session["DatabasePassword"] = value;
}
}
}
Then in your DAL:
class DataAccessLayer : IDataAccessLayer
{
private readonly IDatabaseAuthenticationContext _dbAuthenticationContext;
public DataAccessLayer(IDatabaseAuthenticationContext context)
{
_dbAuthenticationContext = context; //Injected
}
public void ExecuteSomeCommand()
{
using (var conn = new SqlConnection(this.CreateConnectionString()))
{
var cmd = new SqlCommand("SomeCommand");
cmd.CommandType = StoredProcedure;
cmd.Connection = conn;
cmd.ExecuteNonQuery();
}
}
private string CreateConnectionString()
{
return String.Format("Server={0};UID={1};PWD={2}", this.GetServerName(),
_dbAuthenticationContext.DatabaseUserName,
_dbAuthenticationContext.Databasepassword);
}
Then in your composition root:
container.RegisterType<IDatabaseAuthenticationContext, AuthenticationContext>();
container.RegisterType<HttpContextBase, () => new HttpContextWrapper(HttpContext.Current));
container.RegisterType<IDataAccessLayer, DataAccessLayer>();
This way you can pass what you need to the DAL without requiring the DAL to know anything about HttpContext or other web-specific APIs. If you ever write a console application (or other program without a reference to System.Web), you just need to write a different implementation of AuthenticationContext that implements IDatabaseAuthenticationContext.

Retrieving parent/child records (object containing list) from Azure Mobile Services using EF

I have .net 4.5.2 test app playing about with Azure Mobile Services and I'm attempting to store data using the TableController. I have my data types as follows:
public class Run:EntityData
{
public int RunId { get; set; }
public DateTime? ActivityStarted { get; set; }
public DateTime? ActivityCompleted { get; set; }
public List<Lap> LapInformation { get; set; }
public Run()
{
LapInformation = new List<Lap>();
}
}
public class Lap
{
[Key]
public int LapNumber { get; set; }
public int CaloriesBurnt { get; set; }
public double Distance {get; set;}
//Some other basic fields in here
public DateTime? LapActivityStarted { get; set; }
public DateTime? LapActivityCompleted { get; set; }
public Lap()
{
}
In my Startup class I call:
HttpConfiguration config = new HttpConfiguration();
new MobileAppConfiguration()
.UseDefaultConfiguration()
.ApplyTo(config);
And in my MobileServiceContext class:
public class MobileServiceContext : DbContext
{
private const string connectionStringName = "Name=MS_TableConnectionString2";
public MobileServiceContext() : base(connectionStringName)
{
}
public DbSet<Run> Runs { get; set; }
public DbSet<Lap> Laps { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Add(
new AttributeToColumnAnnotationConvention<TableColumnAttribute, string>(
"ServiceTableColumn", (property, attributes) => attributes.Single().ColumnType.ToString()));
}
}
In my controller then, I have:
[MobileAppController]
public class RunController: TableController<Run>
{
protected override void Initialize(HttpControllerContext controllerContext)
{
base.Initialize(controllerContext);
MobileServiceContext context = new MobileServiceContext();
DomainManager = new EntityDomainManager<Run>(context, Request);
}
public IList<Run> GetAllRuns()
{
var runs = context.Runs.Include("LapInformation").ToList();
return runs;
}
public SingleResult<Run> GetRun(string id)
{
return Lookup(id);
}
public async Task<IHttpActionResult> PostRun(Run run)
{
Run current = await InsertAsync(run);
return CreatedAtRoute("Tables", new { id = current.Id }, current);
}
public Task DeleteRun(string id)
{
return DeleteAsync(id);
}
}
I can then POST a record in fiddler which responds with a 201 and the Location of the newly created Item. An Example of the data I'm posting is:
{RunId: 1234, LapInformation:[{LapNumber:1,Distance:0.8, LapActivityStarted: "2017-06-19T00:00:00", LapActivityCompleted: "2017-06-19T00:00:00", CaloriesBurnt: 12}]}
However, when I GET that object, I'm only getting the fields from Run, without the list of Detail records (Lap). Is there anything I have to configure in Entity Framework so that when I GET a Run record from the DB, it also gets and deserializes all associated detail records?
Hopefully that makes sense.
EDIT
Turns out that it is pulling back all the lap information, but when I return it to the client, that information is getting lost.
You can use custom EF query with Include() method instead of Lookup call preferably overload that takes function from System.Data.Entity namespace.
var runs = context.Runs.Include(r => r.LapInformation)
Take a look at https://msdn.microsoft.com/en-us/library/jj574232(v=vs.113).aspx
AFAIK, you could also use the $expand parameter to expand your collections as follows:
GET /tables/Run$expand=LapInformation
Here is my sample, you could refer to it:
You could mark your action with a custom ActionFilterAttribute for automatically adding the $expand property to your query request as follows:
// GET tables/TodoItem
[ExpandProperty("Tags")]
public IQueryable<TodoItem> GetAllTodoItems()
{
return Query();
}
For more details, you could refer to adrian hall's book chapter3 relationships.
EDIT Turns out that it is pulling back all the lap information, but when I return it to the client, that information is getting lost.
I defined the following models in my mobile client:
public class TodoItem
{
public string Id { get; set; }
public string UserId { get; set; }
public string Text { get; set; }
public List<Tag> Tags { get; set; }
}
public class Tag
{
public string Id { get; set; }
public string TagName { get; set; }
}
After execute the following pull operation, I could retrieve the tags as follows:
await todoTable.PullAsync("todoItems", todoTable.CreateQuery());
Note: The Tags data is read-only, you could only update the information in the ToDoItem table.
Additionally, as adrian hall mentioned in Data Access and Offline Sync - The Domain Manager:
I prefer handling tables individually and handling relationship management on the mobile client manually. This causes more code on the mobile client but makes the server much simpler by avoiding most of the complexity of relationships.

Categories

Resources