How to inject dependencies into a MassTransitStateMachine activity? - c#

I'm stuck and the docks for the lib are unhelpful. Given the below saga definition:
public class GetOrdersStateMachine : MassTransitStateMachine<GetOrdersState>
{
public State? FetchingOrdersAndItems { get; private set; }
public Event<GetOrders>? GetOrdersIntegrationEventReceived { get; private set; }
public GetOrdersStateMachine()
{
Initially(
When(GetOrdersIntegrationEventReceived)
.Activity(AddAccountIdToState)
.TransitionTo(FetchingOrdersAndItems));
}
private EventActivityBinder<GetOrdersState, GetOrders> AddAccountIdToState(
IStateMachineActivitySelector<GetOrdersState, GetOrders> sel) =>
sel.OfType<AddAccountIdToStateActivity>();
}
And the below activity definition:
public class AddAccountIdToStateActivity : Activity<GetOrdersState, GetOrders>
{
private readonly IPartnerService _partnerService;
public AddAccountIdToStateActivity(IPartnerService partnerService) => _partnerService = partnerService;
public void Probe(ProbeContext context) =>
context.CreateScope($"GetOrders{nameof(AddAccountIdToStateActivity)}");
public void Accept(StateMachineVisitor visitor) => visitor.Visit(this);
public async Task Execute(
BehaviorContext<GetOrdersState, GetOrders> context,
Behavior<GetOrdersState, GetOrders> next)
{
context.Instance.AccountId = await _partnerService.GetAccountId(context.Data.PartnerId);
await next.Execute(context);
}
public Task Faulted<TException>(
BehaviorExceptionContext<GetOrdersState, GetOrders, TException> context,
Behavior<GetOrdersState, GetOrders> next) where TException : Exception =>
next.Faulted(context);
}
And the below test definition:
var machine = new GetOrdersStateMachine();
var harness = new InMemoryTestHarness();
var sagaHarness = harness.StateMachineSaga<GetOrdersState, GetOrdersStateMachine>(machine);
var #event = new GetOrders("1", new[] {MarketplaceCode.De}, DateTime.UtcNow);
await harness.Start();
try
{
await harness.Bus.Publish(#event);
await harness.Bus.Publish<ListOrdersErrorResponseReceived>(new
{
#event.CorrelationId,
AmazonError = "test"
});
var errorMessages = sagaHarness.Consumed.Select<ListOrdersErrorResponseReceived>().ToList();
var sagaResult = harness.Published.Select<AmazonOrdersReceived>().ToList();
var state = sagaHarness.Sagas.Contains(#event.CorrelationId);
harness.Consumed.Select<GetOrders>().Any().Should().BeTrue();
sagaHarness.Consumed.Select<GetOrders>().Any().Should().BeTrue();
harness.Consumed.Select<ListOrdersErrorResponseReceived>().Any().Should().BeTrue();
errorMessages.Any().Should().BeTrue();
sagaResult.First().Context.Message.IsFaulted.Should().BeTrue();
errorMessages.First().Context.Message.CorrelationId.Should().Be(#event.CorrelationId);
errorMessages.First().Context.Message.AmazonError.Should().Be("test");
state.IsFaulted.Should().BeTrue();
}
finally
{
await harness.Stop();
}
As you can see, the AddAccountToStateActivity has a dependency on the IPartnerService. I can't figure a way to configure that dependency.There's nothing in the docs and neither can I find anything on the github. How do I do it?

Thanks to the help of one of the library's authors I ended up writing this code:
private static (InMemoryTestHarness harness, IStateMachineSagaTestHarness<GetOrdersState, GetOrdersStateMachine> sagaHarness) ConfigureAndGetHarnesses()
{
var provider = new ServiceCollection()
.AddMassTransitInMemoryTestHarness(cfg =>
{
cfg.AddSagaStateMachine<GetOrdersStateMachine, GetOrdersState>().InMemoryRepository();
cfg.AddSagaStateMachineTestHarness<GetOrdersStateMachine, GetOrdersState>();
})
.AddLogging()
.AddSingleton(Mock.Of<IPartnerService>())
.AddSingleton(Mock.Of<IStorage>())
.BuildServiceProvider(true);
var harness = provider.GetRequiredService<InMemoryTestHarness>();
var sagaHarness = provider
.GetRequiredService<IStateMachineSagaTestHarness<GetOrdersState, GetOrdersStateMachine>>();
return (harness, sagaHarness);
}
As you can see I'm registering my mocks with the ServiceProvider.

Related

Conflict with mocking service when seeding database for testing

I have a multi tenant web API where I seed a database with initial data.
I also have a transient IUserService which has a GetCustomerId function to retrieve the current customerId. This service is used in the databaseContext to store the CustomerId foreign key on the created domain entity "under the hood".
So when I seed the database I create a new scope and use a ICurrentUserInitializer to set the CustomerId in the IUserService for that scope, so the CustomerId is valid when the database context stores the entity.
This works just fine in development, but not for testing. Since I want to mock the IUserService when I test, this means that Moq overrides the GetCustomerId. But I only want to mock that service AFTER I've finished seeding the test database.
I've also tried not mocking the IUserService, and instead use a ICurrentUserInitializer for every test that runs, i.e. for every test, create a new scope, set the CustomerId with the ICurrentUserInitializer in that scope, and run the test in that scope, and then reset for the next test. This seems to work, but isn't as flexible when you want to run tests as different users and it doesn't seem as elegant, since I have to write more code to handle the scope correctly.
I Use xUnit, Moq, Respawn, and Microsoft.AspNetCore.Mvc.Testing
DbContext :
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
{
int? customerId = CurrentUser.GetCustomerId();
HandleAuditingBeforeSaveChanges(customerId);
int result = await base.SaveChangesAsync(cancellationToken);
return result;
}
private void HandleAuditingBeforeSaveChanges(int? customerId)
{
foreach (var entry in ChangeTracker.Entries<IMustHaveTenant>().ToList())
{
entry.Entity.CustomerId = entry.State switch
{
EntityState.Added => customerId.Value,
_ => entry.Entity.CustomerId
};
}
}
DatabaseInitializer :
public async Task InitializeApplicationDbForCustomerAsync(Customer Customer, CancellationToken cancellationToken)
{
// First create a new scope
using var scope = _serviceProvider.CreateScope();
// This service injects a CustomerId, so that ICurrentUser retrieves this value, but
// doesn't work, since Moq overrides the value
scope.ServiceProvider.GetRequiredService<ICurrentUserInitializer>()
.SetCurrentCustomerId(customer.Id);
// Then run the initialization in the new scope
await scope.ServiceProvider.GetRequiredService<ApplicationDbSeeder>()
.SeedDatabaseAsync(_dbContext, cancellationToken);
}
CustomWebApplicationFactory:
public class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureAppConfiguration(configurationBuilder =>
{
var integrationConfig = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables()
.Build();
configurationBuilder.AddConfiguration(integrationConfig);
});
builder.ConfigureServices((context, services) =>
{
services
.Remove<DbContextOptions<ApplicationDbContext>>()
.AddDbContext<ApplicationDbContext>((sp, options) =>
{
options.UseSqlServer(context.Configuration.GetConnectionString("DefaultConnection"),
builder => builder.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName));
});
});
builder.ConfigureTestServices(services =>
{
services
.Remove<ICurrentUser>()
.AddTransient(_ => Mock.Of<ICurrentUser>(s =>
s.GetCustomerId() == GetCurrentCustomerId()));
});
}
}
Testing / CollectionFixture :
public class DatabaseCollection : ICollectionFixture<Testing>
{
}
public partial class Testing : IAsyncLifetime
{
private static WebApplicationFactory<Program> _factory = null!;
private static IConfiguration _configuration = null!;
private static IServiceScopeFactory _scopeFactory = null!;
private static Checkpoint _checkpoint = null!;
private static int? _currentCustomerId = null;
public Task InitializeAsync()
{
_factory = new CustomWebApplicationFactory();
_scopeFactory = _factory.Services.GetRequiredService<IServiceScopeFactory>();
_configuration = _factory.Services.GetRequiredService<IConfiguration>();
_checkpoint = new Checkpoint
{
TablesToIgnore = new[] { new Table("__EFMigrationsHistory") },
};
return Task.CompletedTask;
}
public static int? GetCurrentCustomerId()
{
return _currentCustomerId;
}
public static void RunAsDefaultUserAsync()
{
_currentCustomerId = DefaultValues.Customer.Id;
}
public static async Task<TResponse> SendAsync<TResponse>(IRequest<TResponse> request)
{
using var scope = _scopeFactory.CreateScope();
var mediator = scope.ServiceProvider.GetRequiredService<ISender>();
return await mediator.Send(request);
}
public static async Task<TEntity?> FindAsync<TEntity>(params object[] keyValues)
where TEntity : class
{
using var scope = _scopeFactory.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
return await context.FindAsync<TEntity>(keyValues);
}
public static async Task AddAsync<TEntity>(TEntity entity)
where TEntity : class
{
using var scope = _scopeFactory.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
context.Add(entity);
await context.SaveChangesAsync();
}
public static async Task ResetState()
{
await _checkpoint.Reset(_configuration.GetConnectionString("DefaultConnection"));
await _factory.Services.InitializeDatabasesAsync();
_currentCustomerId = null;
}
public Task DisposeAsync()
{
return Task.CompletedTask;
}
}
DepartmentTest:
[Collection("Database collection")]
public class GetDepartmentsTest : BaseTestFixture
{
[Fact]
public async Task ShouldReturnDepartments()
{
RunAsDefaultUserAsync();
var query = new ListDepartmentRequest();
var result = await SendAsync(query);
result.ShouldNotBeNull();
}
[Fact]
public async Task ShouldReturnAllDepartments()
{
RunAsDefaultUserAsync();
await AddAsync(new Department
{
Description = "Department 1",
});
await AddAsync(new Department
{
Description = "Department 2",
});
var query = new ListDepartmentRequest();
var result = await SendAsync(query);
result.ShouldNotBeNull();
result.Count.ShouldBe(2);
}
}
BaseTestFixture:
public class BaseTestFixture : IAsyncLifetime
{
public async Task InitializeAsync()
{
await ResetState();
}
public async Task DisposeAsync()
{
await ResetState();
//return Task.CompletedTask;
}
}

How to configure TestClusterBuilder such that the test cluster has access to SMSProvider?

I am trying to follow their tutorial for tests:
Link to Orleans test docs
But the configuration shown is pretty simplistic:
var builder = new TestClusterBuilder();
var cluster = builder.Build();
cluster.Deploy();
And does not show how to add SMSProvider.
Let's say that I have the following test:
[Fact]
public async Task SaysHelloCorrectly()
{
var hello = _cluster.GrainFactory.GetGrain<ISomeGrain>(Guid.NewGuid(), "<Some stream ID>", "Some.Class.Path");
var guid = Guid.NewGuid();
var streamProvider = _cluster.Client.GetStreamProvider("SMSProvider");
var photoStream = streamProvider.GetStream<string>(guid, "<Some stream ID>");
await photoStream.OnNextAsync("Hi");
Assert.Equal("Hello, World", "Hello, World");
}
Then I get an error in line:
var streamProvider = _cluster.Client.GetStreamProvider("SMSProvider");
Such as:
[xUnit.net 00:00:02.02] Tests.SaysHelloCorrectly [FAIL]
Failed Tests.FilterTests.SaysHelloCorrectly [8 ms]
Error Message:
System.Collections.Generic.KeyNotFoundException : SMSProvider
I think that configuring the ClusterFixture correctly would be part of the solution or maybe the way I am getting the StreamProvider in the unit test is not correct.
All of the code looks like the following:
ClusterCollection:
using System;
using Orleans.TestingHost;
using Xunit;
namespace Tests {
[CollectionDefinition(ClusterCollection.Name)]
public class ClusterCollection : ICollectionFixture<ClusterFixture>
{
public const string Name = "ClusterCollection";
}
}
ClusterFixture:
public class ClusterFixture : IDisposable
{
private sealed class Configurator : ISiloBuilderConfigurator
{
public void Configure(ISiloHostBuilder siloBuilder)
{
siloBuilder.AddSimpleMessageStreamProvider("SMSProvider");
siloBuilder.AddMemoryGrainStorage("PubSubStore");
}
}
public ClusterFixture()
{
var builder = new TestClusterBuilder();
builder.AddSiloBuilderConfigurator<Configurator>();
this.Cluster = builder.Build();
this.Cluster.Deploy();
}
public void Dispose()
{
this.Cluster.StopAllSilos();
}
public TestCluster Cluster { get; private set; }
}
SomeTests:
[Collection(ClusterCollection.Name)]
public class SomeTests
{
private readonly TestCluster _cluster;
public SomeTests(ClusterFixture fixture)
{
_cluster = fixture.Cluster;
}
[Fact]
public async Task SaysHelloCorrectly()
{
var hello = _cluster.GrainFactory.GetGrain<ISomeGrain>(Guid.NewGuid(), "<Some stream ID>", "Some.Class.Path");
var guid = Guid.NewGuid();
var streamProvider = _cluster.Client.GetStreamProvider("SMSProvider");
var photoStream = streamProvider.GetStream<string>(guid, "<Some stream ID>");
await photoStream.OnNextAsync("Hi");
Assert.Equal("Hello, World", "Hello, World");
}
}
The setup needs to be improved. You can check this sample from the official Orleans GitHub page: https://github.com/dotnet/orleans/blob/main/test/TesterInternal/StreamingTests/SMSStreamingTests.cs
So, in your ClusterFixture class you should add the SiloConfigurator:
private class SiloConfigurator : ISiloConfigurator
{
public void Configure(ISiloBuilder hostBuilder) =>
hostBuilder.AddSimpleMessageStreamProvider("SMSProvider")
.AddMemoryGrainStorage("PubSubStore");
}
and the ClientConfiguretor:
private class ClientConfiguretor : IClientBuilderConfigurator
{
public void Configure(IConfiguration configuration, IClientBuilder clientBuilder) =>
clientBuilder.AddSimpleMessageStreamProvider("SMSProvider");
}
And then in your ClusterFixture constructor:
public ClusterFixture()
{
var builder = new TestClusterBuilder();
builder.AddSiloBuilderConfigurator<SiloConfigurator>();
builder.AddClientBuilderConfigurator<ClientConfiguretor>();
this.Cluster = builder.Build();
this.Cluster.Deploy();
}

Dependency Injection - directory inject one class in another class

I have a MailRepository class with the following structire:
public class MailRepository : IMailRepository
{
public MailRepository()
{
}
public async Task SendMail(string subject, string content, string recipientAddress)
{
}
}
I also have a LocalizationReposiory class with the following code:
public class LocalizationRepository : ILocalizationRepository
{
private readonly IStringLocalizer<LocalizationRepository> _localizer = null;
public LocalizationRepository(IStringLocalizer<LocalizationRepository> localizer)
{
_localizer = localizer;
}
public string TranslateSetting(string settingName, params string[] additionalParams)
{
return _localizer.GetString(settingName, additionalParams);
}
}
This is how I call SendMail method in MailRepository from a class:
var subject = _localizationRepository.TranslateSetting("Subject");
var content = _localizationRepository.TranslateSetting("Body");
await _mailRepository.SendMail(subject, content, "xyz#yahoo.com");
This is how dependency injection in startup looks like:
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddSingleton<IMailRepository>(services => new MailRepository());
builder.Services.AddLocalization(opts => { opts.ResourcesPath = "Resources"; });
builder.Services.Configure<RequestLocalizationOptions>(opts =>
{
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("en-US")
};
opts.DefaultRequestCulture = new RequestCulture("en-US");
opts.SupportedCultures = supportedCultures;
opts.SupportedUICultures = supportedCultures;
});
builder.Services.AddSingleton<ILocalizationRepository, LocalizationRepository>();
}
}
Is there a way to inject the LocalizationRepository directly into MailRepository in order to avoid duplicating the following lines of code in multiple classes?
var subject = _localizationRepository.TranslateSetting("Subject");
var content = _localizationRepository.TranslateSetting("Body");
await _mailRepository.SendMail(subject, c,ontent "xyz#yahoo.com");
Inject ILocalizationRepository into MailRepository
public MailRepository(ILocalizationRepository localizationRepo)
{
//set private var
}
and then change how you register it in service provider
builder.Services.AddSingleton<IMailRepository, MailRepository>();

Trigger Async Function on Other Pages by Calling it in Non-Async Constructor in C#

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.

Property injection

I'm trying make a telegram bot with reminder. I'm using Telegram.Bot 14.10.0, Quartz 3.0.7, .net core 2.0. The first version should : get message "reminder" from telegram, create job (using Quartz) and send meaasage back in 5 seconds.
My console app with DI looks like:
Program.cs
static IBot _botClient;
public static void Main(string[] args)
{
// it doesn't matter
var servicesProvider = BuildDi(connecionString, section);
_botClient = servicesProvider.GetRequiredService<IBot>();
_botClient.Start(appModel.BotConfiguration.BotToken, httpProxy);
var reminderJob = servicesProvider.GetRequiredService<IReminderJob>();
reminderJob.Bot = _botClient;
Console.ReadLine();
_botClient.Stop();
// it doesn't matter
}
private static ServiceProvider BuildDi(string connectionString, IConfigurationSection section)
{
var rJob = new ReminderJob();
var sCollection = new ServiceCollection()
.AddSingleton<IBot, Bot>()
.AddSingleton<ReminderJob>(rJob)
.AddSingleton<ISchedulerBot>(s =>
{
var schedBor = new SchedulerBot();
schedBor.StartScheduler();
return schedBor;
});
return sCollection.BuildServiceProvider();
}
Bot.cs
public class Bot : IBot
{
static TelegramBotClient _botClient;
public void Start(string botToken, WebProxy httpProxy)
{
_botClient = new TelegramBotClient(botToken, httpProxy);
_botClient.OnReceiveError += BotOnReceiveError;
_botClient.OnMessage += Bot_OnMessage;
_botClient.StartReceiving();
}
private static async void Bot_OnMessage(object sender, MessageEventArgs e)
{
var me = wait _botClient.GetMeAsync();
if (e.Message.Text == "reminder")
{
var map= new Dictionary<string, object> { { ReminderJobConst.ChatId, e.Message.Chat.Id.ToString() }, { ReminderJobConst.HomeWordId, 1} };
var job = JobBuilder.Create<ReminderJob>().WithIdentity($"{prefix}{rnd.Next()}").UsingJobData(new JobDataMap(map)).Build();
var trigger = TriggerBuilder.Create().WithIdentity($"{prefix}{rnd.Next()}").StartAt(DateTime.Now.AddSeconds(5).ToUniversalTime())
.Build();
await bot.Scheduler.ScheduleJob(job, trigger);
}
}
}
Quartz.net not allow use constructor with DI. That's why I'm trying to create property with DI.
ReminderJob.cs
public class ReminderJob : IJob
{
static IBot _bot;
public IBot Bot { get; set; }
public async Task Execute(IJobExecutionContext context)
{
var parameters = context.JobDetail.JobDataMap;
var userId = parameters.GetLongValue(ReminderJobConst.ChatId);
var homeWorkId = parameters.GetLongValue(ReminderJobConst.HomeWordId);
await System.Console.Out.WriteLineAsync("HelloJob is executing.");
}
}
How can I pass _botClient to reminderJob in Program.cs?
If somebody looks for answer, I have one:
Program.cs (in Main)
var schedBor = servicesProvider.GetRequiredService<ISchedulerBot>();
var logger = servicesProvider.GetRequiredService<ILogger<DIJobFactory>>();
schedBor.StartScheduler();
schedBor.Scheduler.JobFactory = new DIJobFactory(logger, servicesProvider);
DIJobFactory.cs
public class DIJobFactory : IJobFactory
{
static ILogger<DIJobFactory> _logger;
static IServiceProvider _serviceProvider;
public DIJobFactory(ILogger<DIJobFactory> logger, IServiceProvider sp)
{
_logger = logger;
_serviceProvider = sp;
}
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{
IJobDetail jobDetail = bundle.JobDetail;
Type jobType = jobDetail.JobType;
try
{
_logger.LogDebug($"Producing instance of Job '{jobDetail.Key}', class={jobType.FullName}");
if (jobType == null)
{
throw new ArgumentNullException(nameof(jobType), "Cannot instantiate null");
}
return (IJob)_serviceProvider.GetRequiredService(jobType);
}
catch (Exception e)
{
SchedulerException se = new SchedulerException($"Problem instantiating class '{jobDetail.JobType.FullName}'", e);
throw se;
}
}
// get from https://github.com/quartznet/quartznet/blob/139aafa23728892b0a5ebf845ce28c3bfdb0bfe8/src/Quartz/Simpl/SimpleJobFactory.cs
public void ReturnJob(IJob job)
{
var disposable = job as IDisposable;
disposable?.Dispose();
}
}
ReminderJob.cs
public interface IReminderJob : IJob
{
}
public class ReminderJob : IReminderJob
{
ILogger<ReminderJob> _logger;
IBot _bot;
public ReminderJob(ILogger<ReminderJob> logger, IBot bot)
{
_logger = logger;
_bot = bot;
}
public async Task Execute(IJobExecutionContext context)
{
var parameters = context.JobDetail.JobDataMap;
var userId = parameters.GetLongValue(ReminderJobConst.ChatId);
var homeWorkId = parameters.GetLongValue(ReminderJobConst.HomeWordId);
await _bot.Send(userId.ToString(), "test");
}
}

Categories

Resources