I'm building a saga state machine, trimmed down implementation below:
public class DueDiligenceCaseCreateStateMachine : MassTransitStateMachine<DueDiligenceCaseCreateState>
{
public State CreatingCase { get; private set; }
public Event<DueDiligenceCaseCreateCommand> TriggerReceived { get; private set; }
public Event CaseCreationFinished { get; private set; }
public Event CaseCreationFailed { get; private set; }
private readonly ILogger<DueDiligenceCaseCreateStateMachine> _logger;
private readonly IOptions<DueDiligenceCaseCreateSagaOptions> _sagaOptions;
public DueDiligenceCaseCreateStateMachine(
ILogger<DueDiligenceCaseCreateStateMachine> logger,
IOptions<DueDiligenceCaseCreateSagaOptions> sagaOptions)
{
_logger = logger;
_sagaOptions = sagaOptions;
Configure();
BuildProcess();
}
private void Configure()
{
Event(
() => TriggerReceived,
e => e.CorrelateById(x => x.Message.DueDiligenceCaseId));
}
private void BuildProcess()
{
During(
Initial,
When(TriggerReceived)
.TransitionTo(CreatingCase)
.Activity(CreateCase));
}
private EventActivityBinder<DueDiligenceCaseCreateState, DueDiligenceCaseCreateCommand> CreateCase(
IStateMachineActivitySelector<DueDiligenceCaseCreateState, DueDiligenceCaseCreateCommand> sel) =>
sel.OfType<CreateCaseActivity>();
}
And the activity itself is here:
public class CreateCaseActivity : BaseActivity<DueDiligenceCaseCreateState, DueDiligenceCaseCreateCommand>
{
private readonly ICommandHandler<InitializeCaseCommand> _initializeCaseHandler;
private readonly IOptions<ApplicationOptions> _options;
private readonly ILogger<DueDiligenceCaseCreateConsumer> _logger;
public CreateCaseActivity(
ICommandHandler<InitializeCaseCommand> initializeCaseHandler,
IOptions<ApplicationOptions> options,
ILogger<DueDiligenceCaseCreateConsumer> logger)
{
_initializeCaseHandler = initializeCaseHandler;
_options = options;
_logger = logger;
}
public override async Task Execute(
BehaviorContext<DueDiligenceCaseCreateState, DueDiligenceCaseCreateCommand> context,
Behavior<DueDiligenceCaseCreateState, DueDiligenceCaseCreateCommand> next)
{
_logger.LogInformation(
"Consuming {Command} started, case id: {caseid}, creating a case...",
nameof(DueDiligenceCaseCreateCommand),
context.Data.DueDiligenceCaseId);
var initializeCaseCmd = ConvertMessageToCommand(context.Data);
initializeCaseCmd.CanHaveOnlyOneActiveCasePerCustomer = !_options.Value.FeatureToggles.AllowMultipleActiveCasesOnSingleCustomer;
try
{
await _initializeCaseHandler.Handle(initializeCaseCmd);
}
catch
{
}
finally
{
await next.Execute(context);
}
}
private InitializeCaseCommand ConvertMessageToCommand(DueDiligenceCaseCreateCommand message) =>
// returns the command object
}
The state machine has two events for now - CaseCreationFinished and CaseCreationFailed. I'd like to raise the first one in the try clause of the activity and the other one in the catch part. I see the context object passed in as an argument has the Raise method, but the problem is that I can't reach the DueDiligenceCaseCreateStateMachine.CaseCreationFinished from within the activity. Is there a way to do it?
There is a Raise method on BehaviorContext, why not use it?
Updated
You can add a dependency on your activity for the state machine itself, which would give you access to the events.
Related
I´ve got a configuration-class which holds database connection-strings per mandant. But only with the requests parameters the mandant will be clear. So I want to inject the right DbContext contitional by mandant.
So far I´ve the following problem:
public class MessageController : IMessageController
{
private readonly IMessageParser _parser;
private readonly ILogger _log;
private readonly IMessageProcessor _receiver;
public MessageController(IMessageParser parser, IMessageProcessor receiver, ILogger log)
{
_parser = parser;
_log = log;
_receiver = receiver;
}
public async Task<Response> MessageReceivedEvent(Request request)
{
if (!_parser.TryParseMessage(request.SomeInlineData, out var mandant))
{
_log.LogError("The given Message could not be parsed");
throw new InvalidOperationException("The given Message could not be parsed");
}
// what to do with the mandant?
_receive.Received(request);
return new Response();
}
}
The receiver may has the following logic:
public class MessageProcessor : IMessageProcessor
{
// this database should be injected dependend on the current mandant
private readonly DbContext _database;
public MessageProcessor(DbContext database)
{
_database = database;
}
public void Received(Request request)
{
// Do fancy stuff
_database.SaveChanges();
}
}
Now here the ConfigureServices:
services.AddDbContext<DbContextX>((provider, options) => options.UseSqlite($"Data
Source={Path.GetFullPath("How to get the right mandant connection string?")}"))
.Configure<MandantConfiguration>(Configuration.GetSection(nameof(MandantConfiguration)))
Here the configuration class:
public class MandantConnection : IMandantConnection
{
public string DatabaseConnection { get; set; }
}
public class MandantConfiguration : IMandantConfiguration
{
public Dictionary<Mandant, MandantConnection> Mandants { get; set; }
}
EDIT:
The DbContext is injected as Scoped, so I think it should be possible to change the Connection-String per Scope but I don´t know how.
The trick is to use the HttpContext within the request.
So far here my solution for the given Problem:
public class MessageController : IMessageController
{
private readonly IMessageParser _parser;
private readonly ILogger _log;
private readonly IMessageProcessor _receiver;
private readonly IHttpContextAccessor _accessor;
public MessageController(IMessageParser parser, IMessageProcessor receiver, ILogger log, IHttpContextAccessor accessor)
{
_parser = parser;
_log = log;
_receiver = receiver;
_accessor = accessor;
}
public async Task<Response> MessageReceivedEvent(Request request)
{
if (!_parser.TryParseMessage(request.SomeInlineData, out var mandant))
{
_log.LogError("The given Message could not be parsed");
throw new InvalidOperationException("The given Message could not be parsed");
}
// ---> Thats to do
_accessor.HttpContext.Items[nameof(Mandant)] = mandant;
_receive.Received(request);
return new Response();
}
}
Then I´ve implemented a MandantService, which injects the Accessor also:
public class MandantenService : IMandantenService
{
public IMandantConnection CurrentConfiguration { get; set; }
private readonly MandantConfiguration _configuration;
public MandantenService(IOptions<MandantConfiguration> options, IHttpContextAccessor accessor)
{
_configuration = options.Value;
CurrentConfiguration = _configuration.Mandants[Enum.Parse<Mandant>(accessor.HttpContext.Items[nameof(Mandant)].ToString())];
}
}
Then I can use this service within the DbContext:
public VdvKaDbContext(DbContextOptions<VdvKaDbContext> options, IMandantenService mandantenService)
: base(options)
{
_mandantenService = mandantenService;
...
}
And configure the Sqlite Database in the OnConfigure-Method:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite($"Data Source={Path.GetFullPath(_mandantenService.CurrentConfiguration.DatabaseConnection)}");
}
And boom every call of scoped database will be the specific mandant database:
using var scope = _provider.CreateScope();
return scope.ServiceProvider.GetService<DbContext>();
I am building a inbox transactional pattern approach for "intercepting" messages using an oberserver and filter to log them to a database. The idea is if a consumer failed to let a background worker periodically check the database for any records where the Integration event has failed, retrieve them and re-execute them.
The data being stored in the DB looks like this:
public class InboxMessage
{
public long Id { get; private set; }
public DateTime CreatedDate { get; private set; }
public string CreatedUser { get; private set; }
public DateTime EditedDate { get; private set; }
public string EditedUser { get; private set; }
public string MessageType { get; private set; }
public string? ConsumerType { get; private set; }
public string Data { get; private set; }
public Guid EventNumber { get; private set; }
public EventStatus Status { get; private set; }
}
The idea is to retrieve all messages with a Status = Failed, use reflection (or perhaps something else?) to Deserialise the "Data" prop to the MessageType. The ConsumerType would then be used to re-execute the consumer.
I use a Filter to log the initial message (before being transfered to the consumer):
public class InboxPatternConsumerFilter<T> : IFilter<ConsumeContext<T>> where T : class
{
private readonly IntegrationEventsContext _context;
private readonly ILogger _logger;
private const string MassTransitDynamicTypeName = "MassTransit.DynamicInternal.";
public InboxPatternConsumerFilter(ILoggerFactory logger, IntegrationEventsContext context)
{
_logger = logger.CreateLogger("InboxPatternConsumerFilter");
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public async Task Send(ConsumeContext<T> context, IPipe<ConsumeContext<T>> next)
{
if (context.Message is IIntegrationEvent #event)
{
try
{
_logger.LogInformation("Integration event is type of - {generic}. Applying inbox pattern.", nameof(IGenericIntegrationEvent));
var message = new InboxMessage(
#event.EditedUser ?? "unknown",
context.Message.GetType().FullName?.Replace(MassTransitDynamicTypeName, string.Empty) ?? string.Empty,
null,
System.Text.Json.JsonSerializer.Serialize(context.Message),
#event.EventId,
EventStatus.Received);
_context.InboxMessages.Add(message);
await _context.SaveChangesAsync(context.CancellationToken);
}
catch (Exception ex)
{
// exception is catched to ensure the consumer can still continue.
_logger.LogError(ex, "Failed to create inbox message");
}
}
await next.Send(context);
}
public void Probe(ProbeContext context) {}
}
My reason for using a filter would be to check the EventNumber to confirm whether this message already exists in the DB, this should allow me to prevent sending this message to the consumer to resolve the idempotent issue in cases where we are using Retry mechanism for failed messaged.
I use a basic ReceiveObserver to update the messages as follows:
public class ReceiveObserver : IReceiveObserver
{
private readonly IntegrationEventsContext _context;
private readonly ILogger<ReceiveObserver> _logger;
public ReceiveObserver(IntegrationEventsContext context, ILogger<ReceiveObserver> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public async Task PostConsume<T>(ConsumeContext<T> context, TimeSpan duration, string consumerType) where T : class
{
try
{
if (context.Message is IIntegrationEvent #event)
{
var message = await _context.InboxMessages.FirstOrDefaultAsync(x => x.EventNumber == #event.EventId);
if (message is not null)
{
message.Update(EventStatus.Completed, "post-consumer", consumerType);
_context.Update(message);
await _context.SaveChangesAsync();
var typeTest = System.Reflection.Assembly
.GetEntryAssembly()?
.GetType(consumerType);
}
else
{
_logger.LogWarning("Inbox Message not found");
}
}
}
catch (Exception ex)
{
_logger.LogError("An error occurred trying to update the Message's complete status", ex);
}
// called when the message was consumed, once for each consumer
}
public async Task ConsumeFault<T>(ConsumeContext<T> context, TimeSpan elapsed, string consumerType, Exception exception) where T : class
{
if (context.Message is IIntegrationEvent #event)
{
var message = await _context.InboxMessages.FirstOrDefaultAsync(x => x.EventNumber == #event.EventId);
if (message is not null)
{
message.Update(EventStatus.Failed, "consumer-fault", consumerType);
_context.Update(message);
await _context.SaveChangesAsync();
}
else
{
_logger.LogWarning("Inbox Message not found");
}
}
// called when the message is consumed but the consumer throws an exception
}
public Task ReceiveFault(ReceiveContext context, Exception exception)
{
// TODO: Get the message id and update the status in db.
// called when an exception occurs early in the message processing, such as deserialization, etc.
return Task.CompletedTask;
}
}
The idea is then to use a background service to check for any failed messages as follows:
public class InboxMessageService : IHostedService
{
private readonly IBusControl _bus;
private readonly IntegrationEventsContext _context;
private readonly ILogger<InboxMessageService> _logger;
private readonly IServiceProvider _serviceProvider;
public InboxMessageService(
IBusControl bus,
IntegrationEventsContext context,
ILogger<InboxMessageService> logger,
IServiceProvider serviceProvider)
{
_bus = bus ?? throw new ArgumentNullException(nameof(context)); ;
_context = context ?? throw new ArgumentNullException(nameof(context));
_logger = logger ?? throw new ArgumentNullException(nameof(context));
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
}
public async Task StartAsync(CancellationToken cancellationToken)
{
List<InboxMessage> messages = await _context.InboxMessages
.Where(x => x.Status != EventStatus.Completed)
.ToListAsync(cancellationToken);
foreach(InboxMessage message in messages)
{
try
{
if (message.ConsumerType is null)
{
_logger.LogWarning("Unable to find the consumer type to start this message.");
continue;
}
var typeTest = System.Reflection.Assembly
.GetEntryAssembly()?
.GetType(message.ConsumerType);
if (typeTest is null)
{
throw new Exception();
}
var constructor = typeTest.GetConstructors().First(); // We can always assume that the consumer will contain a ctor
var parameters = new List<object?>();
foreach (var param in constructor.GetParameters())
{
var service = _serviceProvider.GetService(param.ParameterType);//get instance of the class
parameters.Add(service);
}
var obj = Activator.CreateInstance(typeTest, parameters.ToArray());
// TODO: fiqure out how to create a ConsumeContext<T> message from the DB data
typeTest.GetMethod("Consume")?.Invoke(obj, System.Reflection.BindingFlags.InvokeMethod, Type.DefaultBinder, null, null);
}
catch (Exception ex)
{
// ...
}
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
The part I am struggling with is the TODO in the background service to figure out how to essentially recreate the Message (using the MessageType and Data props) and invoke the Consumer (using the ConsumerType).
I am using MassTransit 7.2.2 in a .NET Core application with RabbitMQ(for local development) and SQS(for deployment) where a single message processing can result in multiple new messages getting created and processed.
All the messages share the same base type
public class BaseMessage : CorrelatedBy<Guid>
{
public BaseMessage()
{
CorrelationId = Guid.NewGuid();
CreationDate = DateTime.UtcNow;
}
public Guid CorrelationId { get; init; }
public DateTime CreationDate { get; }
public Guid? ConversationId { get; set; }
}
The basic flow of processing is same for all messages, there is a Service per Consumer.
public class FirstMessage : BaseMessage
{
}
public class FirstConsumer : IConsumer<FirstMessage>
{
private readonly ILogger<FirstConsumer> _logger;
private readonly FirstService _service;
public FirstConsumer(ILogger<FirstConsumer> logger, FirstService service)
{
_logger = logger;
_service = service;
}
public Task Consume(ConsumeContext<FirstMessage> context)
{
_logger.LogInformation($"FirstConsumer CorrelationId: {context.CorrelationId} and ConversationId: {context.ConversationId} and InitiatorId: {context.InitiatorId}");
_service.Process(context.Message);
return Task.CompletedTask;
}
}
public class FirstService
{
private readonly IBusControl _busControl;
private readonly ILogger<FirstService> _logger;
public FirstService(IBusControl busControl, ILogger<FirstService> logger)
{
_busControl = busControl;
_logger = logger;
}
public Task Process(FirstMessage firstMessage)
{
var secondMessage = new SecondMessage();
_busControl.Publish(secondMessage);
return Task.CompletedTask;
}
}
The above code is an example and the actual code base has 30+ consumers and all have the same pattern, i.e there is a Service per Consumer and the message is passed to the Service for processing.
I am trying to implement a solution for tracing messages end to end by using the Ids.
ConversationId - Unique Id for tracing logs of all Consumers in a graph
CorrelationId - Unique Id for tracing logs within a Consumer
InitiatorId - Parent Id
There is a message processing graph that looks like
FirstConsumer -> SecondConsumer -> ThirdConsumer.
I have the following Filters
ConsumeFilter
public class SimpleConsumeMessageFilter<TContext, TMessage> : IFilter<TContext>
where TContext : class, ConsumeContext<TMessage>
where TMessage : class
{
public SimpleConsumeMessageFilter()
{
}
public async Task Send(TContext context, IPipe<TContext> next)
{
LogContext.PushProperty("CorrelationId", context.CorrelationId);
LogContext.PushProperty("ConversationId", context.ConversationId);
LogContext.PushProperty("InitiatorId", context.InitiatorId);
await next.Send(context);
}
public void Probe(ProbeContext context)
{
context.CreateScope("consume-filter");
}
}
public class SimpleConsumeMessagePipeSpec<TConsumer, TMessage> : IPipeSpecification<ConsumerConsumeContext<TConsumer, TMessage>>
where TConsumer : class
where TMessage : class
{
public void Apply(IPipeBuilder<ConsumerConsumeContext<TConsumer, TMessage>> builder)
{
builder.AddFilter(new SimpleConsumeMessageFilter<ConsumerConsumeContext<TConsumer, TMessage>, TMessage>());
}
public IEnumerable<ValidationResult> Validate()
{
return Enumerable.Empty<ValidationResult>();
}
}
public class SimpleConsumePipeSpecObserver : IConsumerConfigurationObserver
{
public void ConsumerConfigured<TConsumer>(IConsumerConfigurator<TConsumer> configurator)
where TConsumer : class
{
}
public void ConsumerMessageConfigured<TConsumer, TMessage>(IConsumerMessageConfigurator<TConsumer, TMessage> configurator)
where TConsumer : class
where TMessage : class
{
configurator.AddPipeSpecification(new SimpleConsumeMessagePipeSpec<TConsumer, TMessage>());
}
}
PublishFilter
public class SimplePublishMessageFilter<TMessage> : IFilter<PublishContext<TMessage>> where TMessage : class
{
public SimplePublishMessageFilter()
{
}
public async Task Send(PublishContext<TMessage> context, IPipe<PublishContext<TMessage>> next)
{
if (context.Headers.TryGetHeader("ConversationId", out object #value))
{
var conversationId = Guid.Parse(#value.ToString());
context.ConversationId = conversationId;
}
else
{
if (context.Message is BaseMessage baseEvent && !context.ConversationId.HasValue)
{
context.ConversationId = baseEvent.ConversationId ?? Guid.NewGuid();
context.Headers.Set("ConversationId", context.ConversationId.ToString());
}
}
await next.Send(context);
}
public void Probe(ProbeContext context)
{
context.CreateScope("publish-filter");
}
}
public class SimplePublishMessagePipeSpec<TMessage> : IPipeSpecification<PublishContext<TMessage>> where TMessage : class
{
public void Apply(IPipeBuilder<PublishContext<TMessage>> builder)
{
builder.AddFilter(new SimplePublishMessageFilter<TMessage>());
}
public IEnumerable<ValidationResult> Validate()
{
return Enumerable.Empty<ValidationResult>();
}
}
public class SimplePublishPipeSpecObserver : IPublishPipeSpecificationObserver
{
public void MessageSpecificationCreated<TMessage>(IMessagePublishPipeSpecification<TMessage> specification)
where TMessage : class
{
specification.AddPipeSpecification(new SimplePublishMessagePipeSpec<TMessage>());
}
}
Added to config via
x.UsingRabbitMq((context, cfg) =>
{
cfg.ConnectConsumerConfigurationObserver(new SimpleConsumePipeSpecObserver());
cfg.ConfigurePublish(ppc =>
{
ppc.ConnectPublishPipeSpecificationObserver(new SimplePublishPipeSpecObserver());
});
cfg.UseDelayedMessageScheduler();
cfg.ConfigureEndpoints(context);
cfg.Host("localhost", rmq =>
{
rmq.Username("guest");
rmq.Password("guest");
});
});
With the above approach the 'CorrelationId' header is lost when the SecondConsumer's filters are run.
I have tried the following change and it seems to flow the Ids across the Consumers.
However, taking this approach will impact large sections of code / tests that rely on the IBusControl interface. I am keeping this as a backup option in case I can't find any other solution.
public class FirstService
{
private readonly ILogger<FirstService> _logger;
public FirstService(ILogger<FirstService> logger)
{
_logger = logger;
}
public Task Process( ConsumeContext<FirstMessage> consumeContext)
{
var secondMessage = new SecondMessage();
consumeContext.Publish(secondMessage);
return Task.CompletedTask;
}
}
Question: Is there a way to share the Context data between Consumers while using IBusControl for sending / publishing messages ?
Many thanks
As explained in the documentation, consumers (and their dependencies) must use one of the following when sending/publishing messages:
ConsumeContext, typically within the consumer itself
IPublishEndpoint or ISendEndpointProvider, typically used by scoped dependencies of the consumer
IBus, last resort, as all contextual data is lost from the inbound message
As for your final question, "Is there a way to share the Context data between Consumers while using IBusControl for sending / publishing messages?" the answer is no. The consume context would be needed to access any of the contextual data.
In my App, there is an Async Function ProcessOffer(). When I called it in Constructor as ProcessOffer, it works but synchronously. I want to call this Function in constructor asynchronously.
The ProcessOffer() is a function implemented in CredentialViewModel, but I want that, It should be triggered asynchronously everywhere on the App (IndexViewModel e.t.c).
If I'm on the IndexPage, and Web Application sends a request to Mobile Application, ProcessOffer(), should be triggered.. actually what ProcessOffer does is, that it asks the user to enter a PIN, if it's correct, it sends back a response to the Web Application.
I've tried answers from other Posts, but they returned the Error Autofac.Core.DependencyResolutionException: 'An exception was thrown while activating App.Name, when I sends a request to Mobile App from Web Application.
The Solutions I tried.
1- https://stackoverflow.com/a/64012442/14139029
2- Task.Run(() => ProcessOffer()).Wait();
3- ProcessOffer().GetAwatier().GetResult();
CredentialViewModel.cs
namespace Osma.Mobile.App.ViewModels.Credentials
{
public class CredentialViewModel : ABaseViewModel
{
private readonly CredentialRecord _credential;
private readonly ICredentialService _credentialService;
private readonly IAgentProvider _agentContextProvider;
private readonly IConnectionService _connectionService;
private readonly IMessageService _messageService;
private readonly IPoolConfigurator _poolConfigurator;
[Obsolete]
public CredentialViewModel(
IUserDialogs userDialogs,
INavigationService navigationService,
ICredentialService credentialService,
IAgentProvider agentContextProvider,
IConnectionService connectionService,
IMessageService messageService,
IPoolConfigurator poolConfigurator,
CredentialRecord credential
) : base(
nameof(CredentialViewModel),
userDialogs,
navigationService
)
{
_credential = credential;
_credentialService = credentialService;
_agentContextProvider = agentContextProvider;
_connectionService = connectionService;
_messageService = messageService;
_poolConfigurator = poolConfigurator;
_credentialState = _credential.State.ToString();
if (_credentialState == "Offered")
{
ProcessOffer();
}
}
[Obsolete]
public async Task ProcessOffer()
{
foreach (var item in _credential.CredentialAttributesValues)
{
await SecureStorage.SetAsync(item.Name.ToString(), item.Value.ToString());
}
var RegisteredPIN = await SecureStorage.GetAsync("RegisteredPIN");
string PIN = await App.Current.MainPage.DisplayPromptAsync("Enter PIN", null, "Ok", "Cancel", null, 6, Keyboard.Numeric);
if (PIN == RegisteredPIN)
{
try
{
//await _poolConfigurator.ConfigurePoolsAsync();
var agentContext = await _agentContextProvider.GetContextAsync();
var credentialRecord = await _credentialService.GetAsync(agentContext, _credential.Id);
var connectionId = credentialRecord.ConnectionId;
var connectionRecord = await _connectionService.GetAsync(agentContext, connectionId);
(var request, _) = await _credentialService.CreateRequestAsync(agentContext, _credential.Id);
await _messageService.SendAsync(agentContext.Wallet, request, connectionRecord);
await DialogService.AlertAsync("Request has been sent to the issuer.", "Success", "Ok");
}
catch (Exception e)
{
await DialogService.AlertAsync(e.Message, "Error", "Ok");
}
}
else if (PIN != RegisteredPIN && PIN != null)
{
DialogService.Alert("Provided PIN is not correct");
}
}
#region Bindable Command
[Obsolete]
public ICommand ProcessOfferCommand => new Command(async () => await ProcessOffer());
public ICommand NavigateBackCommand => new Command(async () =>
{
await NavigationService.PopModalAsync();
});
#endregion
#region Bindable Properties
private string _credentialState;
public string CredentialState
{
get => _credentialState;
set => this.RaiseAndSetIfChanged(ref _credentialState, value);
}
#endregion
}
}
IndexViewModel.cs
namespace Osma.Mobile.App.ViewModels.Index
{
public class IndexViewModel : ABaseViewModel
{
private readonly IConnectionService _connectionService;
private readonly IMessageService _messageService;
private readonly IAgentProvider _agentContextProvider;
private readonly IEventAggregator _eventAggregator;
private readonly ILifetimeScope _scope;
public IndexViewModel(
IUserDialogs userDialogs,
INavigationService navigationService,
IConnectionService connectionService,
IMessageService messageService,
IAgentProvider agentContextProvider,
IEventAggregator eventAggregator,
ILifetimeScope scope
) : base(
"Index",
userDialogs,
navigationService
)
{
_connectionService = connectionService;
_messageService = messageService;
_agentContextProvider = agentContextProvider;
_eventAggregator = eventAggregator;
_scope = scope;
}
public override async Task InitializeAsync(object navigationData)
{
await base.InitializeAsync(navigationData);
}
public class Post
{
public string Success { get; set; }
public string firstname { get; set; }
}
[Obsolete]
public async Task ScanVerification(object sender, EventArgs e)
{
// Code
}
public async Task SettingsPage(SettingsViewModel settings) => await NavigationService.NavigateToAsync(settings, null, NavigationType.Modal);
#region Bindable Command
public ICommand SettingsPageCommand => new Command<SettingsViewModel>(async (settings) =>
{
await SettingsPage(settings);
});
[Obsolete]
public ICommand ScanVerificationCommand => new Command(async () => await ScanVerification(default, default));
#endregion
}
}
App.xml.cs
[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
namespace Osma.Mobile.App
{
public partial class App : Application
{
public new static App Current => Application.Current as App;
public static IContainer Container { get; set; }
// Timer to check new messages in the configured mediator agent every 10sec
private readonly Timer timer;
private static IHost Host { get; set; }
public App()
{
InitializeComponent();
timer = new Timer
{
Enabled = false,
AutoReset = true,
Interval = TimeSpan.FromSeconds(10).TotalMilliseconds
};
timer.Elapsed += Timer_Elapsed;
}
public App(IHost host) : this() => Host = host;
public static IHostBuilder BuildHost(Assembly platformSpecific = null) =>
XamarinHost.CreateDefaultBuilder<App>()
.ConfigureServices((_, services) =>
{
services.AddAriesFramework(builder => builder.RegisterEdgeAgent(
options: options =>
{
options.AgentName = "Mobile Holder";
options.EndpointUri = "http://11.222.333.44:5000";
options.WalletConfiguration.StorageConfiguration =
new WalletConfiguration.WalletStorageConfiguration
{
Path = Path.Combine(
path1: FileSystem.AppDataDirectory,
path2: ".indy_client",
path3: "wallets")
};
options.WalletConfiguration.Id = "MobileWallet";
options.WalletCredentials.Key = "SecretWalletKey";
options.RevocationRegistryDirectory = Path.Combine(
path1: FileSystem.AppDataDirectory,
path2: ".indy_client",
path3: "tails");
// Available network configurations (see PoolConfigurator.cs):
options.PoolName = "sovrin-test";
},
delayProvisioning: true));
services.AddSingleton<IPoolConfigurator, PoolConfigurator>();
var containerBuilder = new ContainerBuilder();
containerBuilder.RegisterAssemblyModules(typeof(CoreModule).Assembly);
if (platformSpecific != null)
{
containerBuilder.RegisterAssemblyModules(platformSpecific);
}
containerBuilder.Populate(services);
Container = containerBuilder.Build();
});
protected override async void OnStart()
{
await Host.StartAsync();
// View models and pages mappings
var _navigationService = Container.Resolve<INavigationService>();
_navigationService.AddPageViewModelBinding<MainViewModel, MainPage>();
_navigationService.AddPageViewModelBinding<RegisterViewModel, RegisterPage>();
_navigationService.AddPageViewModelBinding<IndexViewModel, IndexPage>();
_navigationService.AddPageViewModelBinding<SettingsViewModel, SettingsPage>();
_navigationService.AddPageViewModelBinding<CredentialsViewModel, CredentialsPage>();
_navigationService.AddPageViewModelBinding<CredentialViewModel, CredentialPage>();
if (Preferences.Get(AppConstant.LocalWalletProvisioned, false))
{
await _navigationService.NavigateToAsync<MainViewModel>();
}
else
{
await _navigationService.NavigateToAsync<ProviderViewModel>();
}
timer.Enabled = true;
}
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
// Check for new messages with the mediator agent if successfully provisioned
if (Preferences.Get(AppConstant.LocalWalletProvisioned, false))
{
Device.BeginInvokeOnMainThread(async () =>
{
try
{
var context = await Container.Resolve<IAgentProvider>().GetContextAsync();
await Container.Resolve<IEdgeClientService>().FetchInboxAsync(context);
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
});
}
}
protected override void OnSleep() =>
// Stop timer when application goes to background
timer.Enabled = false;
protected override void OnResume() =>
// Resume timer when application comes in foreground
timer.Enabled = true;
}
}
Look, you have public override async Task InitializeAsync(object navigationData) method in the IndexViewModel class. I suppose the framework invoke it to initialize the view model. So, thus CredentialViewModel inherits same ABaseViewModel anyway, why wouldn't you just override InitializeAsync in your CredentialViewModel and call ProcessOffer from it?
public CredentialViewModel(
IUserDialogs userDialogs,
INavigationService navigationService,
ICredentialService credentialService,
IAgentProvider agentContextProvider,
IConnectionService connectionService,
IMessageService messageService,
IPoolConfigurator poolConfigurator,
CredentialRecord credential
) : base(
nameof(CredentialViewModel),
userDialogs,
navigationService
)
{
_credential = credential;
_credentialService = credentialService;
_agentContextProvider = agentContextProvider;
_connectionService = connectionService;
_messageService = messageService;
_poolConfigurator = poolConfigurator;
_credentialState = _credential.State.ToString();
}
public override async Task InitializeAsync(object navigationData)
{
if (_credentialState != "Offered") return;
await ProcessOffer();
}
Anyway, you have to avoid calling asynchronous operations in constructors.
So in my View Models, if i need to initialize asynchronously I usually pass OnAppearing() to the view model instead of the constructor. Someone can tell me if this is unwise but it solved my needs.
Code Behind View:
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class CredentialView : ContentPage
{
public CredentialView()
{
InitializeComponent();
BindingContext = new CredentialViewModel();
}
protected override void OnAppearing()
{
base.OnAppearing();
((CredentialViewModel)BindingContext).OnAppearing();
}
}
View Model:
public class CredentialViewModel : INotifyPropertyChanged
{
public CredentialViewModel ()
{
}
public async void OnAppearing()
{
await ProcessOffer();
}
}
Also just FYI, the biggest gotcha i've experienced with asynchronous code on xamarin is to use BeginInvokeOnMainThread. It's my understanding that touched UI should be executed on main thread! (probably why they call it the UI thread. haha)
Example:
Device.BeginInvokeOnMainThread(() =>
{
observableCollection.Clear();
});
As you've already asked before, kicking off async code in the constructor of a class can be a can of worms. You should instead consider doing either:
Starting ProcessOffer in a lifecycle method instead. For instance when the View is showing call ProcessOfferCommand.
Using fire and forget to not block the constructor: Task.Run(ProcessOffer) you should probably avoid this though.
Use something like NotifyTask Stephen Cleary describes here: https://learn.microsoft.com/en-us/archive/msdn-magazine/2014/march/async-programming-patterns-for-asynchronous-mvvm-applications-data-binding you can find the complete code for it here: https://github.com/StephenCleary/Mvvm.Async/blob/master/src/Nito.Mvvm.Async/NotifyTask.cs
Use a CreateAsync pattern with a private constructor. However, this doesn't work well with Dependency Injection usually. Doing IO intensive work during resolution isn't really the correct place to do it.
In my opinion using either 1. or 3. would be the best solutions, perhaps leaning towards a combination of 1. using NotifyTask that can notify you when it is done loading.
I'm trying to set up multiple hosted services that all share some base functionality, but differ in some configuration that they use to set things up (like connection strings, urls, values, or whatever). I would like to achieve something like this in Program.cs:
services.AddHostedService<CustomHostedService1>(config =>
{
config.Value = 1234;
});
services.AddHostedService<CustomHostedService2>(config =>
{
config.Value = 5678;
});
The base class and derived classes would look something like this:
public abstract class BaseHostedService : BackgroundService
{
public MyConfigObject Config { get; set; }
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
ConfigureBaseClass(Config);
DoWork();
}
private void ConfigureBaseClass(MyConfigObject config)
{
// Use config to do something
}
public abstract void DoWork();
}
public class CustomHostedService1 : BaseHostedService
{
private readonly ISomeService1 _someService;
public CustomHostedService1(ISomeService1 someService)
{
_someService = someService;
}
public override void DoWork()
{
// Do some work here
_someService.DoWork();
}
}
public class CustomHostedService2 : BaseHostedService
{
private readonly ISomeService2 _someService;
private readonly ILogger<CustomHostedService2> _logger;
public CustomHostedService2(ILogger<CustomHostedService2> logger, ISomeService2 someService)
{
_someService = someService;
_logger = logger;
}
public override void DoWork()
{
// Do some work here
_someService.DoWork();
}
}
What would be a good pattern to achieve this? Note that the two derived classes might have different dependencies.