Tenant session is lost in IRealTimeNotifier - c#

We are trying to achieve a scenario where user can subscribe to notifications via channels.
For example, user can subscribe to notifications via Email, SMS, Real Time Notifications, etc.
For now, in order to achieve email notifications, I am implementing the IRealTimeNotifier in this way:
public class EmailNotifier : IRealTimeNotifier, ITransientDependency
{
private readonly IEmailSender _emailSender;
private readonly ISettingManager _settingManager;
public IAbpSession AbpSession { get; set; }
public EmailNotifier(IEmailSender emailSender, ISettingManager settingManager, IAbpSession abpSession)
{
this._emailSender = emailSender;
this._settingManager = settingManager;
this.AbpSession = NullAbpSession.Instance;
}
public async Task SendNotificationsAsync(UserNotification[] userNotifications)
{
List<Task> sendEmails = new List<Task>();
string fromAddress = this._settingManager.GetSettingValueForTenant(EmailSettingNames.Smtp.UserName, Convert.ToInt32(this.AbpSession.TenantId));
foreach (UserNotification notification in userNotifications)
{
notification.Notification.Data.Properties.TryGetValue("toAddresses", out object toAddresses);
notification.Notification.Data.Properties.TryGetValue("subject", out object sub);
notification.Notification.Data.Properties.TryGetValue("body", out object content);
notification.Notification.Data.Properties.TryGetValue("toAddresses", out object toAddresses);
List<string> toEmails = toAddresses as List<string> ;
string subject = Convert.ToString(sub);
string body = Convert.ToString(content);
toEmails.ForEach(to =>
{
sendEmails.Add(this._emailSender.SendAsync(fromAddress, to, subject, body));
});
}
await Task.Run(() =>
{
sendEmails.ForEach(async task =>
{
try
{
await task.ConfigureAwait(false);
}
catch (Exception)
{
// TODO #1: Add email to background job to be sent later.
// TODO #2: Add circuit breaker to email sending mechanism
/*
Email sending is failing because of two possible reasons.
1. Email addresses are wrong.
2. SMTP server is down.
3. Break the circuit while the SMTP server is down.
*/
// TODO #3 (Optional): Notify tenant admin about failure.
// TODO #4: Remove throw statement for graceful degradation.
throw;
}
});
});
}
}
The problem is with the IAbpSession, whether I inject it via property or constructor, at the time of execution of this notifier, the response has already been returned and the TenantId in this session is null, so the email is being sent with host configurations and not tenant configuration.
Similarly, I need to implement IRealTimeNotifier for SMS. I think I can reuse the SignalRRealTimeNotifier from ABP, but the tenantId in session is being set to null.
This is where the publisher is being called:
public class EventUserEmailer : IEventHandler<EntityCreatedEventData<Event>>
{
public ILogger Logger { get; set; }
private readonly IEventManager _eventManager;
private readonly UserManager _userManager;
private readonly IAbpSession _abpSession;
private readonly INotificationPublisher _notiticationPublisher;
public EventUserEmailer(
UserManager userManager,
IEventManager eventManager,
IAbpSession abpSession,
INotificationPublisher notiticationPublisher)
{
_userManager = userManager;
_eventManager = eventManager;
_notiticationPublisher = notiticationPublisher;
_abpSession = abpSession;
Logger = NullLogger.Instance;
}
[UnitOfWork]
public virtual void HandleEvent(EntityCreatedEventData<Event> eventData)
{
// TODO: Send email to all tenant users as a notification
var users = _userManager
.Users
.Where(u => u.TenantId == eventData.Entity.TenantId)
.ToList();
// Send notification to all subscribed uses of tenant
_notiticationPublisher.Publish(AppNotificationNames.EventCreated);
}
}
Can anybody recommend a better way? Or point anything out that we are doing wrong here.
I have not thought about how to handle DI of these particular notifiers yet. For testing purposes, I have given a named injection in my module like this:
IocManager.IocContainer.Register(
Component.For<IRealTimeNotifier>()
.ImplementedBy<EmailNotifier>()
.Named("Email")
.LifestyleTransient()
);
Current ABP version: 3.7.1
This is the information I have until now. If anything is needed, you can ask in comments.

// Send notification to all subscribed uses of tenant
_notificationPublisher.Publish(AppNotificationNames.EventCreated);
If you publish notifications like this, ABP's default implementation enqueues a background job.
There is no session in a background job.
You can get the tenantId like this:
var tenantId = userNotifications[0].Notification.TenantId;
using (AbpSession.Use(tenantId, null))
{
// ...
}

Related

ASP.NET Core 6 SignalR hub store data

I'm working on a simple chat using SignalR. At the moment I'm trying associate a users connection id with their identity user. I want to do this to prevent a user from impersonating another user by manually calling the hub functions.
My hub looks something like this:
public static class MessageContext
{
public static string RECEIVE = "ReceiveMessage";
public static string REGISTER = "Register";
public static string SEND = "SendMessage";
}
public class ChatHub : Hub
{
public const string HUBURL = "/api/ChatSignal";
Dictionary<string, string> _userContext;
public ChatHub()
{
_userContext = new Dictionary<string, string>();
}
public override Task OnConnectedAsync()
{
var ConnectionId = Context.ConnectionId;
var Username = Context.User.Identity.Name;
_userContext.Add(ConnectionId, Username);
Groups.AddToGroupAsync(ConnectionId, Username);
return base.OnConnectedAsync();
}
public async Task SendAll(string user, string message)
{
var ConnectionId = Context.ConnectionId;
message = HttpUtility.HtmlEncode(message);
await Clients.All.SendAsync(MessageContext.RECEIVE, _userContext[ConnectionId], message);
}
public Task SendMessage(string sender, string receiver, string message)
{
var ConnectionId = Context.ConnectionId;
message = HttpUtility.HtmlEncode(message);
return Clients.Group(receiver).SendAsync(MessageContext.RECEIVE, _userContext[ConnectionId], message);
}
public override async Task OnDisconnectedAsync(Exception e)
{
var ConnectionId = Context.ConnectionId;
var Username = Context.User.Identity.Name;
_userContext.Remove(ConnectionId);
await base.OnDisconnectedAsync(e);
}
}
My problem is that after when I call these functions, the dictionary gets set to null. After looking around for a while I found on MSDN that, hubs are "Transient", so each hub method call is executed on a new hub instance. This is a problem if I want to save ConnectionId:Identity.Name.
How can I use a dictionary to store this data for each hub instance?
To fix the null issue remove the initialization of _userContext in the constructor.
And change the line Dictionary<string, string> _userContext; -> private static ConcurrentDictionary<string, string> _userContext = new ConcurrentDictionary<string, string>();
This is how your dictionary state will be retained across different hub instances. And ConcurrentDictionary will make it thread-safe.
But it is not a very scaleable solution. If you are really making a production-grade chat application, try to use something like Redis Cache for such state management.

Round Robin message assignment to partition not working for messages without key in asp.net core

Trying to send messages to all the partition in round-robin fashion but all the messages are going into the last partition. Can anyone please help me with that?
I am using Confluent.Kafka nuget package. My producer Configuration -
"ProducerConfiguration": {
"bootstrap.servers": "localhost:9092"
}
And my Kafka producer class -
public class Publisher
{
private ProducerConfig _producerConfig;
public Publisher(IOptions<ApplicationSetting> _applicationSetting)
{
var producerConfig = _applicationSetting.Value.KafkaConfiguration.ProducerConfiguration;
_producerConfig = new ProducerConfig(producerConfig);
}
public async Task<DeliveryResult<Null, TValue>> Publish<TValue>(TValue message, string topic = null)
{
using var producer = new ProducerBuilder<Null, TValue>(_producerConfig)
.SetValueSerializer(new JsonSerializer<TValue>())
.Build();
var topicName = String.IsNullOrEmpty(topic) ? message.GetType().Name : topic;
return await producer.ProduceAsync(topicName, new Message<Null, TValue>() { Value = message });
}
}
Publishing the message like -
public class DemoHandler : IRequestHandler<SendMail, string>
{
private readonly Publisher _publisher;
public DemoHandler(Publisher publisher)
{
_publisher = publisher;
}
public async Task<string> Handle(SendMail message, CancellationToken cancellationToken)
{
await _publisher.Publish(message);
return "Message sent";
}
}
All the messages are going to the last partition only -
Thanks in advance.
I added this answer
https://stackoverflow.com/a/71791802/628908
I faced this same issue. In this case the issue will be because you're re-creating a new IProducer instance each time you call your Publish method
If you could make sure the IProducer instance is the same every time you call Publish, that will let the producer to decide which partition send the message to.

Using HttpContext in a background task after response has been completed

A user can trigger a long-running job by sending a request to an ASP.NET Core controller. Currently, the controller executes the job and then sends a 200 OK response. The problem is that the client has to wait rather long for the response.
This is why I am currently trying to process the job in a background task. I am using an IBackgroundTaskQueue where all jobs are stored and an IHostedService that processes the jobs whenever a new one is enqueued. It is similar to the code in the Microsoft documentation.
But the job does need access to the database and therefore the user has to authenticate using Active Directory. Hence, I need access to the HttpContext.User property in the background task. Unfortunately, the HttpContext is disposed when the response is sent and before the processing of the job begins.
Demonstration
public class Job
{
public Job(string message)
{
Message = message;
}
public string Message { get; }
}
The controller enqueues a new job in the task queue.
[HttpGet]
public IActionResult EnqueueJob()
{
var job = new Job("Hello World");
this.taskQueue.QueueBackgroundWorkItem(job);
return Accepted();
}
public class BackgroundTaskQueue : IBackgroundTaskQueue
{
private ConcurrentQueue<Job> jobs = new ConcurrentQueue<Job>();
private SemaphoreSlim signal = new SemaphoreSlim(0);
public void QueueBackgroundWorkItem(Job job)
{
jobs.Enqueue(job);
signal.Release();
}
public async Task<Job> DequeueAsync(CancellationToken cancellationToken)
{
await signal.WaitAsync(cancellationToken);
jobs.TryDequeue(out var job);
return job;
}
}
The IHostedService creates a new JobRunner for each job it dequeues. I'm using a IServiceScopeFactory here to have dependency injection available. JobRunner also has a lot more dependencies in the real code.
public class JobRunnerService : BackgroundService
{
private readonly IServiceScopeFactory serviceScopeFactory;
private readonly IBackgroundTaskQueue taskQueue;
public JobRunnerService(IServiceScopeFactory serviceScopeFactory, IBackgroundTaskQueue taskQueue)
{
this.serviceScopeFactory = serviceScopeFactory;
this.taskQueue = taskQueue;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (stoppingToken.IsCancellationRequested == false)
{
var job = await taskQueue.DequeueAsync(stoppingToken);
using (var scope = serviceScopeFactory.CreateScope())
{
var serviceProvider = scope.ServiceProvider;
var runner = serviceProvider.GetRequiredService<JobRunner>();
runner.Run(job);
}
}
}
}
public class JobRunner
{
private readonly ILogger<JobRunner> logger;
private readonly IIdentityProvider identityProvider;
public JobRunner(ILogger<JobRunner> logger, IIdentityProvider identityProvider)
{
this.logger = logger;
this.identityProvider= identityProvider;
}
public void Run(Job job)
{
var principal = identityProvider.GetUserName();
logger.LogInformation($"{principal} started a new job. Message: {job.Message}");
}
}
public class IdentityProvider : IIdentityProvider
{
private readonly IHttpContextAccessor httpContextAccessor;
public IdentityProvider(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public string GetUserName()
=> httpContextAccessor.HttpContext.User.Identity.Name; // throws NullReferenceException
}
Now, when sending a request, a NullReferenceException is thrown in JobRunner.Run() because httpContextAccessor.HttpContext is null.
What I've tried
I haven't had a good idea yet how to approach this problem. I know that it would be possible to copy the necessary information from the HttpContext, but don't know how to make them available to dependency injection services.
I thought that maybe I could create a new IServiceProvider that uses the services of an old one, but replaces the implementation for IHttpContextAccesor, but it does not seem to be possible.
How can I use the HttpContext in the background task although the response has been completed?

The Notification System of Abp is not run .What wrong with it?

Notification System used to send some message to specific users.But global event not avaible in js file.
code in js
abp.event.on('abp.notifications.received', function (userNotification) {
console.log("userNotification", userNotification);
});
code in application
public class NotificationTestService : AcsAppServiceBase, INotificationTestService
{
private INotificationPublisher _notificationPublisher;
private readonly IRepository<User, long> _userRepository;
public NotificationTestService(INotificationPublisher notificationPublisher, IRepository<User, long> userRepository)
{
_notificationPublisher = notificationPublisher;
_userRepository = userRepository;
}
public async void NotificationTest()
{
string message = "Test Message";
var currentUser = (await this.GetCurrentUserAsync()).ToUserIdentifier();
_notificationPublisher.Publish("NotificationTest", new MessageNotificationData(message), severity: NotificationSeverity.Info, userIds: new[] { currentUser });
}
}
notification data 1
notification data 2
I found the notification data in database,but event is not trigger in js file .No exception throw.
Thank you for your any help.

Google OAuth Data Storage for tokens

Our app needs to let users sync sync their Google calendar on to our internal events module but I'm stuck on doing the authorization and storing it on to our DB. I was following this sample Google OAuth for reference but in that sample it was stated to implement your own DataStore that uses EF, I went ahead and tried it but I can't seem to make it to work and I've been stuck for a couple of weeks now, any help will be greatly appreciated. Thanks!
I had something like this before:
public class TokenDataStore : IDataStore
{
private readonly IUnitOfWork _unitOfWork;
private readonly IUserRepository _userRepository;
private readonly IBusinessRepository _businessRepository;
public TokenDataStore(IUnitOfWork unitOfWork, IUserRepository userRepository, IBusinessRepository businessRepository)
{
this._unitOfWork = unitOfWork;
this._userRepository = userRepository;
this._businessRepository = businessRepository;
}
//Todo: Add IdataStore Members
public static string GenerateStoredKey(string key, Type t)
{
return string.Format("{0}-{1}", t.FullName, key);
}
}
Some more information:
- MVC 4
-.Net 4.5
- EF 5
- Google APIs
EDIT
Ok, so I made some progress this past couple of hours, I was finally able to make my own AppFlowMetaData
private readonly IAuthorizationCodeFlow flow;
private readonly GoogleAPI _API;
private readonly IGoogleAPIRepository _googleAPIRepository;
private readonly IUnitOfWork _unitOfWork;
public AppFlowMetadata(GoogleAPI API, IGoogleAPIRepository googleAPIRepository, IUnitOfWork unitOfWork)
{
this._API = API;
this._googleAPIRepository = googleAPIRepository;
this._unitOfWork = unitOfWork;
this.flow =
new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = ConfigurationManager.AppSettings["GP_Key"],
ClientSecret = ConfigurationManager.AppSettings["GP_Secret"]
},
Scopes = new[] { CalendarService.Scope.Calendar },
DataStore = new TokenDataStore(_API, _googleAPIRepository, _unitOfWork)
});
}
also my own data store class:
private readonly IGoogleAPIRepository _googleAPIRepository;
private readonly IUnitOfWork _unitOfWork;
private readonly GoogleAPI _model;
public TokenDataStore(GoogleAPI model, IGoogleAPIRepository googleAPIRepository, IUnitOfWork unitOfWork)
{
this._googleAPIRepository = googleAPIRepository;
this._unitOfWork = unitOfWork;
this._model = model;
}
Now I encounter a problem during the call back. So, when a user signs in using their account, in my data store class, I save the token as a string in the database and when the code gets to the part where I get the token from my model the data type passed is a string and not the usual Token Response. Here is the complete code for my data store:
public class TokenDataStore : IDataStore
{
private readonly IGoogleAPIRepository _googleAPIRepository;
private readonly IUnitOfWork _unitOfWork;
private readonly GoogleAPI _model;
public TokenDataStore(GoogleAPI model, IGoogleAPIRepository googleAPIRepository, IUnitOfWork unitOfWork)
{
this._googleAPIRepository = googleAPIRepository;
this._unitOfWork = unitOfWork;
this._model = model;
}
public System.Threading.Tasks.Task StoreAsync<T>(string key, T value)
{
var serialized = NewtonsoftJsonSerializer.Instance.Serialize(value);
if(serialized.Contains("access_token"))
{
_model.TokenString = serialized;
_googleAPIRepository.Save(_model, EntityState.Modified);
_unitOfWork.Commit();
}
return TaskEx.Delay(0);
}
public System.Threading.Tasks.Task DeleteAsync<T>(string key)
{
_model.TokenString = "";
_googleAPIRepository.Save(_model, EntityState.Modified);
_unitOfWork.Commit();
return TaskEx.Delay(0);
}
public Task<T> GetAsync<T>(string key)
{
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();
try
{
tcs.SetResult(NewtonsoftJsonSerializer.Instance.Deserialize<T>(_model.TokenString));
}
catch(Exception ex)
{
tcs.SetException(ex);
}
return tcs.Task;
}
public System.Threading.Tasks.Task ClearAsync()
{
return TaskEx.Delay(0);
}
}
Kevin, it is not clear what isn't working..
Could be:
Tokens are not being stored in your database
Authentication is not happening when accessing users resources at later time
I assume it is the latter, so there are two challenges you need to fix:
Token expiration is making Database invalid
Tokens are not being fed back into googleauthenticator when accessing users resources
Things you need to do are to ensure you allow the access to be offline so the server has access to the resources while the user is not present or session out of scope. Read chapter "offline access" here how to do this: Using oAuth for WebApplications
Make sure you validate that the tokens are still valid when feeding them back into the authenticator. If not you need to use refresh token to renew authentication. Validating token
You will need to ensure the googleauthenticator is called with the tokens from you Database to ensure your code has access.
Hope I assumed the correct problem... ;)
You can use the FileDataStore (https://code.google.com/p/google-api-dotnet-client/source/browse/Src/GoogleApis.DotNet4/Apis/Util/Store/FileDataStore.cs), that stores its data in files and not EntityFramework.
Regarding your question - I didn't get it, what is your exact problem?
You need more details on how to inject that data store into your flow object? Or you help with EF? ... Please provide more information.
Try storing an object with the following parameters in your Entity Framework Repository.
public string access_token { get; set; }
public string token_type { get; set; }
public long? expires_in { get; set; }
public string refresh_token { get; set; }
public string Issued { get; set; }
I'm retrieving my tokens from an Web Api which contains an Entity Framework Repository. I retrieve my object as a JSON string. Use Google.Api.Json.NewtonsoftJsonSerializer to deserialize the Json string.
public Task<T> GetAsync<T>(string key)
{
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();
try
{
string dataStoreView = this.httpClient.Get(uri).ResponseBody;
tcs.SetResult(Google.Apis.Json.NewtonsoftJsonSerializer.Instance.Deserialize<T>(dataStoreView));
}
catch (Exception ex)
{
tcs.SetException(ex);
}
return tcs.Task;
}

Categories

Resources