Managing DbContext correctly in a multi thread app - c#

I've read that I shouldn't be using same db context in a multi thread app.
so here is my scenario: I have 2 functions that I run for each years, and each function call sub functions multiple times depending on the row count.
The goal here is : If there are new records add it to the database otherwise update some fields of the record.
What I do is:
1- await StaticSettings.CACHED_DATA.FillAll(); Caches the POCO classes I need into lists. (I'm doing this because I thought that it would be faster to check the records from a cached context rather than using linq on dbcontext multiple times.
2- I run the below code. and Inside it I compare data with cached data
await GetAllData(cookie, datTypes.Select(x => x.DMT_CODE).ToList(), "me", year.Y_YEAR);
await GetAllData(cookie, datTypes.Select(x => x.DMT_CODE).ToList(), "they", year.Y_YEAR);
3- When all tasks are done I call to save changes and dispose objects..
await StaticSettings.CACHED_DATA.ProcessTasks(); //Saves
so What I would like to know is if I can rely on this code..
Trace.WriteLine($#"Caching data");
await StaticSettings.CACHED_DATA.FillAll();
Trace.WriteLine($#"Caching done");
var ts = years.Select(year => Task.Run(async () =>
{
Trace.WriteLine($"Login Start {year.Y_YEAR}");
var cookie = await MahkemeWebUtils.ForceLoginWebClient(user.MU_USERNAME, user.MU_PASSWORD);
Trace.WriteLine($"Login End {year.Y_YEAR}");
await GetAllData(cookie, datTypes.Select(x => x.DMT_CODE).ToList(), "me", year.Y_YEAR);
await GetAllData(cookie, datTypes.Select(x => x.DMT_CODE).ToList(), "they", year.Y_YEAR);
//GC.Collect();
}))
.ToList();
await Task.WhenAll(ts);
await StaticSettings.CACHED_DATA.ProcessTasks(); //Saves
UPDATE
public class CACHED_DATA
{
public AppStructMahkeme _db;
public List<DAVALAR> DavaLarToProcess { get; set; } = new List<DAVALAR>();
public List<KAZALAR> KazaList = new List<KAZALAR>();
public List<DAVA_TURLERI> DavaTurList = new List<DAVA_TURLERI>();
public List<DAVA_SCALE> DavaScaleList = new List<DAVA_SCALE>();
public List<STATUSES> StatusList = new List<STATUSES>();
public List<DAVA_KONULARI> DavakonuList = new List<DAVA_KONULARI>();
public List<AVUKATLAR> AvukatList = new List<AVUKATLAR>();
public List<EVRAK_TYPES> EvrakTypeList = new List<EVRAK_TYPES>();
public List<SENDERS> SendersList = new List<SENDERS>();
public List<ICRA_STATUS> IcraStatusList = new List<ICRA_STATUS>();
public List<DAVALAR> DavaList = new List<DAVALAR>();
public List<ISTIDA_TURLERI> IstidaTurList = new List<ISTIDA_TURLERI>();
public List<ISTIDALAR> IstidaList = new List<ISTIDALAR>();
public async Task FillAll()
{
_db = new AppStructMahkeme();
DavaLarToProcess = new List<DAVALAR>();
//_db.Configuration.LazyLoadingEnabled = false;
//KazaList = null;
KazaList = await _db.KAZALAR.ToListAsync();
DavaTurList = await _db.DAVA_TURLERI.ToListAsync();
DavaScaleList = await _db.DAVA_SCALE.ToListAsync();
StatusList = await _db.STATUSES.ToListAsync();
AvukatList = await _db.AVUKATLAR.ToListAsync();
EvrakTypeList = await _db.EVRAK_TYPES.ToListAsync();
SendersList = await _db.SENDERS.ToListAsync();
IcraStatusList = await _db.ICRA_STATUS.ToListAsync();
DavakonuList = await _db.DAVA_KONULARI.ToListAsync();
DavaList = await _db.DAVALAR.ToListAsync();
IstidaTurList = await _db.ISTIDA_TURLERI.ToListAsync();
IstidaList = await _db.ISTIDALAR.ToListAsync();
}
public async Task ProcessTasks()
{
if(DavaLarToProcess.Count!=0)
_db.DAVALAR.AddRange(DavaLarToProcess);
Trace.WriteLine("saving");
await _db.SaveChangesAsync();
Trace.WriteLine("saved");
_db.Dispose();
DavaLarToProcess = null;
KazaList = null;
DavaTurList = null;
DavaScaleList = null;
StatusList = null;
AvukatList = null;
EvrakTypeList = null;
SendersList = null;
IcraStatusList = null;
DavakonuList = null;
DavaList = null;
IstidaTurList = null;
IstidaList = null;
}
}

Related

How to write unit test for a MediatR handler C#

I'm trying to write unit test using Moq framework for one of my MediatR handler. I have the following code. Briefly, what this handler does is that it queries for the given key and returns a response that contains key and its value using EF Core. Also there is a basic cache mechanism. If the key found in cache it is pulled from cache and returned.
Handler
public GetConfigByKeyRequestHandler(MyContext context, ICacheProvider cacheProvider, IOptions<CacheConfigs> cacheConfigs)
{
this.context = context;
this.cacheProvider = cacheProvider;
this.cacheConfigs = cacheConfigs?.Value;
}
public async Task<ConfigResponse> Handle(GetConfigByKeyRequest request, CancellationToken cancellationToken)
{
ConfigResponse config;
if (!await cacheProvider.ExistsAsync(request.Key))
{
config = await context.Configs
.Where(x.ConfigKey.Equals(request.Key))
.Select(x =>
new ConfigResponse {
ConfigKey = x.ConfigKey,
ConfigValue = x.ConfigValue
})
.FirstOrDefaultAsync(cancellationToken);
if (config is not null)
{
await cacheProvider.PutAsync(new CacheItem<ConfigResponse>(request.Key, config), new CacheOptions
{
ExpireAfter = TimeSpan.FromMinutes(cacheConfigs.ExpireAfterInMinutes).TotalMilliseconds,
ExpireInactive = TimeSpan.FromMinutes(cacheConfigs.ExpireInActiveInMinutes).TotalMilliseconds
});
}
return config;
}
config = await cacheProvider.PullAsync<ConfigResponse>(request.Key);
return config;
}
I have thought that I should cover 2 different scenarios:
When the key found in the cache
When the key is not found in cache and it's returned from DbContext.
Unit tests
private Mock<ICacheProvider> cacheProviderMock;
private IOptions<CacheConfigs> cacheConfigs;
public GetConfigByKeyRequestHandlerTests()
{
cacheProviderMock = new Mock<ICacheProvider>();
cacheConfigs = Options.Create(
new CacheConfigs
{
ExpireAfterInMinutes = 3,
ExpireInActiveInMinutes = 3
});
}
[Fact]
public async Task GetConfigByKeyHandler_WhenKeyIsCached_ShouldReturnConfigByKey()
{
// arrange
var options = new DbContextOptionsBuilder<MyContext>().UseInMemoryDatabase("MyInMemoryDatabase").Options;
var configItems = Enumerable.Range(0, 5).Select(x => new Config
{
ConfigKey = $"key{x}",
ConfigValue = $"value{x}"
});
using (var context = new MyContext(options))
{
await context.Configs.AddRangeAsync(configItems);
await context.SaveChangesAsync();
}
using (var context = new MyContext(options))
{
cacheProviderMock.Setup(x => x.ExistsAsync(It.IsAny<string>())).Returns(Task.FromResult(true));
cacheProviderMock.Setup(x => x.PullAsync<ConfigResponse>("key2"))
.Returns(Task.FromResult(new ConfigResponse
{
ConfigKey = "key2",
ConfigValue = "value2"
}));
var getConfigByKeyHandler = new GetConfigByKeyRequestHandler(context, cacheProviderMock.Object, cacheConfigs);
var getConfigByKeyRequest = new GetConfigByKeyRequest("key2");
// act
var result = await getConfigByKeyHandler.Handle(getConfigByKeyRequest, CancellationToken.None);
// assert
Assert.NotNull(result);
Assert.Equal("key2", result.ConfigKey);
}
}
...
...
With the same logic, I have one more test for the other scenario that when key is not cached
...
...
[Fact]
public async Task GetConfigByKeyHandler_WhenKeyIsNotCached_ShouldReturnConfigByKey()
{
// arrange
var options = new DbContextOptionsBuilder<MyContext>().UseInMemoryDatabase("MyInMemoryDatabase").Options;
var configItems = Enumerable.Range(0, 5).Select(x => new Config
{
ConfigKey = $"key{x}",
ConfigValue = $"value{x}"
});
using (var context = new MyContext(options))
{
await context.Configs.AddRangeAsync(configItems);
await context.SaveChangesAsync();
}
using (var context = new MyContext(options))
{
cacheProviderMock.Setup(x => x.ExistsAsync(It.IsAny<string>())).Returns(Task.FromResult(false));
var getConfigByKeyHandler = new GetConfigByKeyRequestHandler(context, cacheProviderMock.Object, cacheConfigs);
var getConfigByKeyRequest = new GetConfigByKeyRequest("key2");
// act
var result = await getConfigByKeyHandler.Handle(getConfigByKeyRequest, CancellationToken.None);
// assert
Assert.NotNull(result);
Assert.Equal("key2", result.ConfigKey);
}
}
I have written 2 unit tests that covers the scenarios that I have mentioned above, but I'm not sure that they are reasonable ones and I'm not sure it should be tested like this way. Do you have any suggestions what/how should I write tests for the handler I shared above?

Linq call to stored procedure returning to quickly

I'm attempting to call this method:
private async Task<int> RemoveAllCurrentEntries()
{
using (var ent = new WebPortalEntities())
{
// remove all existing price file entries based on a corporationid
var NewOrderIdOut = new SqlParameter(Settings.Default.CorporationId.ToString(),
System.Data.SqlDbType.UniqueIdentifier);
NewOrderIdOut.Direction = System.Data.ParameterDirection.Output;
return await ent.Database.ExecuteSqlCommandAsync(
"exec [dbo].[DeleteAllCorpCustomersFromCustomerPrice] #NewOrderIdOut output",
NewOrderIdOut);
}
}
From this method:
private async Task ExecutePriceFile()
{
try
{
var pricefile = Path.Combine(
Settings.Default.PriceFileDirectory,
Settings.Default.PriceFileName);
// verify file exists other no point in running the rest of the code
if (!File.Exists(pricefile)) return;
var parsedfile = ParsePriceFile(pricefile);
if (!parsedfile.Any()) return;
using (var ent = new WebnPortalEntities())
{
var deleteEntries = RemoveAllCurrentEntries();
if (deleteEntries == Task.CompletedTask)
{
foreach (var priceFile in parsedfile)
{
var pf = new CustomerPrice();
pf.CorporationId = Settings.Default.CorporationId;
pf.CustomerPriceId = Guid.NewGuid();
pf.AccountNumber = priceFile.AccountNumber;
pf.BranchNumber = priceFile.BranchNumber;
pf.BudgetBalance = priceFile.BudgetBalance;
pf.BudgetRate = priceFile.BudgetRate;
pf.ContractDepositQuantity = priceFile.ContractDepositQuantity;
pf.ContractGasPercent = priceFile.ContractGasPercent;
pf.ContractGasQuantity = priceFile.ContractGasQuantity;
pf.ContractGasTenths = priceFile.ContractGasTenths;
pf.CreditLimit = priceFile.CreditLimit;
pf.CurrentDate = priceFile.CurrentDate;
pf.CurrentDiscountDays = priceFile.CurrentDiscountDays;
pf.CurrentDiscountRate = priceFile.CurrentDiscountRate;
//pf.CurrentDiscountRate2 = columns[22].ToDecimal();
pf.CurrentDiscountRateDays = priceFile.CurrentDiscountRateDays;
pf.CurrentDiscountRateType = priceFile.CurrentDiscountType;
pf.CurrentDiscountType = priceFile.CurrentDiscountType;
// pf.CurrentPrice2 = columns[19];
pf.CurrentPrice = priceFile.CurrentPrice;
pf.LastPaymentDate = priceFile.LastPaymentDate;
pf.NonBudgetBalance = priceFile.NonBudgetBalance;
pf.OldestDiscountDays = priceFile.OldestDiscountDays;
pf.OldestDiscountRate = priceFile.OldestDiscountRate;
pf.OldestDiscountType = priceFile.OldestDiscountType;
pf.OldestPrice = priceFile.OldestPrice;
pf.PastDueBalance = priceFile.PastDueBalance;
pf.PaymentAmount = priceFile.PaymentAmount;
pf.PreviousDate = priceFile.PreviousDate;
pf.PreviousDiscountDays = priceFile.PreviousDiscountDays;
pf.PreviousDiscountRate = priceFile.PreviousDiscountRate;
pf.PreviousDiscountType = priceFile.PreviousDiscountType;
pf.PreviousPrice = priceFile.PreviousPrice;
pf.ProductCode = priceFile.ProductCode;
pf.IsActive = priceFile.Status.ToActive();
pf.UnitOfIssue = priceFile.UnitOfIssue;
ent.CustomerPrices.Add(pf);
}
ent.SaveChanges();
}
else
{
throw new Exception(deleteEntries.Result.ToString());
}
}
}
catch (Exception exception)
{
Console.WriteLine(exception);
throw;
}
}
I'm not having the code wait until the task completes, it immediately returns and issues this message:
Id = 11, Status = WaitingForActivation, Method = "{null}", Result =
"{Not yet computed}"
I want it to wait until it's complete then return. Any ideas what I am doing wrong?
await RemoveAllCurrentEntries();
Or (if ur main function is not async)
Var _task = RemoveAllCurrentEntries();
_task.Wait();
Var result = _task.Result;

CosmosDB Entity with the specified id does not exist

I am developing an ASP.NET Core MVC API to call resources in an Azure Cosmos DB. When I try to perform a GET for any specific ID, I receive DocumentClientException: Entity with the specified id does not exist in the system. I can confirm that the entity does exist in the system, and the connection is successful because I can successfully perform other methods and requests. The partition key is _id .
Debugging with breakpoints in Visual Studio, I can see where the correct ID is received at the API, but I can't confirm what specifically it is sending to Azure
The controller methods: (the ID field is a random string of numbers and text)
//controller is MoviesController decorated with [Route(api/[controller])]
//sample GET is to localhost:port/api/Movies/5ca6gdwndkna99
[HttpGet("{id}")]
public async Task<MoviesModel> Get(string id)
{
MoviesModel movie = await _persistence.GetMovieAsync(id);
return movie;
}
The data handling method:
public async Task<MoviesModel> GetMovieAsync(string Id)
{
string _id = Id;
RequestOptions options = new RequestOptions();
options.PartitionKey = new PartitionKey(_id);
var documentUri = UriFactory.CreateDocumentUri(_databaseId, "movies", Id);
Document result = await _client.ReadDocumentAsync(documentUri,options);
return (MoviesModel)(dynamic)result;
}
Other methods, like getting a list of all movies and returning to a table are working fine, so we can rule out network issues
public async Task<List<MoviesModel>> GetMoviesAsync()
{
var documentCollectionUri = UriFactory.CreateDocumentCollectionUri(_databaseId, "movies");
// build the query
var feedOptions = new FeedOptions() { EnableCrossPartitionQuery = true };
var query = _client.CreateDocumentQuery<MoviesModel>(documentCollectionUri, "SELECT * FROM movies", feedOptions);
var queryAll = query.AsDocumentQuery();
// combine the results
var results = new List<MoviesModel>();
while (queryAll.HasMoreResults)
{
results.AddRange(await queryAll.ExecuteNextAsync<MoviesModel>());
}
return results;
}
public async Task<List<GenresModel>> GetGenresAsync()
{
await EnsureSetupAsync();
var documentCollectionUri = UriFactory.CreateDocumentCollectionUri(_databaseId, "genres");
// build the query
var feedOptions = new FeedOptions() { EnableCrossPartitionQuery = true };
var query = _client.CreateDocumentQuery<GenresModel>(documentCollectionUri, "SELECT * FROM genres", feedOptions);
var queryAll = query.AsDocumentQuery();
// combine the results
var results = new List<GenresModel>();
while (queryAll.HasMoreResults)
{
results.AddRange(await queryAll.ExecuteNextAsync<GenresModel>());
}
return results;
}
Firstly, I would suggest to re-look at your cosmosDb design once, bcz of the following reasons...
Problems:
If your _id is random string of numbers and text, then its not good
to have the entire _id as your partition key, bcz this would create a
new partition for each entry.(although azure will range parition it
later)
Querying just by partition key is not efficient, for pin point
queries we should have both partition key and row key.
Solution:
Make the first one or two letters of your _id as your partition key. (so your partitions will be finite).
Make your _id as your row key.
If your _id = "abwed123asdf", then your query should be..
RequestOptions options = new RequestOptions();
options.PartitionKey = new PartitionKey(_id.Substring(0,1));
options.RowKey = _id;
This way, your look up will pin point to the exact required entry with the help of partition and row key. (saves lot of RUs)
Please refer docs for choosing a better partition keys for your needs https://learn.microsoft.com/en-us/azure/cosmos-db/partitioning-overview
I was able to get this to work by completely refactoring to the dotnet v3 SDK. My code for the solution is in the comments of the gitHub link:
using Microsoft.Azure.Cosmos;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using VidlyAsp.DataHandlers;
namespace VidlyAsp.DataHandlers
{
public class PersistenceNew
{
private static string _endpointUri;
private static string _primaryKey;
private CosmosClient cosmosClient;
private CosmosDatabase database;
private CosmosContainer movieContainer;
private CosmosContainer genreContainer;
private string containerId;
private string _databaseId;
public PersistenceNew(Uri endpointUri, string primaryKey)
{
_databaseId = "Vidly";
_endpointUri = endpointUri.ToString();
_primaryKey = primaryKey;
this.GetStartedAsync();
}
public async Task GetStartedAsync()
{
// Create a new instance of the Cosmos Client
this.cosmosClient = new CosmosClient(_endpointUri, _primaryKey);
database = await cosmosClient.Databases.CreateDatabaseIfNotExistsAsync(_databaseId);
CosmosContainer moviesContainer = await GetOrCreateContainerAsync(database, "movies");
CosmosContainer genresContainer = await GetOrCreateContainerAsync(database, "genres");
movieContainer = moviesContainer;
genreContainer = genresContainer;
}
public async Task<GenresModel> GetGenre(string id)
{
var sqlQueryText = ("SELECT * FROM c WHERE c._id = {0}", id).ToString();
var partitionKeyValue = id;
CosmosSqlQueryDefinition queryDefinition = new CosmosSqlQueryDefinition(sqlQueryText);
CosmosResultSetIterator<GenresModel> queryResultSetIterator = this.genreContainer.Items.CreateItemQuery<GenresModel>(queryDefinition, partitionKeyValue);
List<GenresModel> genres = new List<GenresModel>();
while (queryResultSetIterator.HasMoreResults)
{
CosmosQueryResponse<GenresModel> currentResultSet = await queryResultSetIterator.FetchNextSetAsync();
foreach (GenresModel genre in currentResultSet)
{
genres.Add(genre);
}
}
return genres.FirstOrDefault();
}
public async Task<MoviesModel> GetMovie(string id)
{
var sqlQueryText = "SELECT * FROM c WHERE c._id = '" + id + "'";
var partitionKeyValue = id;
CosmosSqlQueryDefinition queryDefinition = new CosmosSqlQueryDefinition(sqlQueryText);
CosmosResultSetIterator<MoviesModel> queryResultSetIterator = this.movieContainer.Items.CreateItemQuery<MoviesModel>(queryDefinition, partitionKeyValue);
List<MoviesModel> movies = new List<MoviesModel>();
while (queryResultSetIterator.HasMoreResults)
{
CosmosQueryResponse<MoviesModel> currentResultSet = await queryResultSetIterator.FetchNextSetAsync();
foreach (MoviesModel movie in currentResultSet)
{
movies.Add(movie);
}
}
return movies.FirstOrDefault();
}
/*
Run a query (using Azure Cosmos DB SQL syntax) against the container
*/
public async Task<List<MoviesModel>> GetAllMovies()
{
List<MoviesModel> movies = new List<MoviesModel>();
// SQL
CosmosResultSetIterator<MoviesModel> setIterator = movieContainer.Items.GetItemIterator<MoviesModel>(maxItemCount: 1);
while (setIterator.HasMoreResults)
{
foreach (MoviesModel item in await setIterator.FetchNextSetAsync())
{
movies.Add(item);
}
}
return movies;
}
public async Task<List<GenresModel>> GetAllGenres()
{
List<GenresModel> genres = new List<GenresModel>();
// SQL
CosmosResultSetIterator<GenresModel> setIterator = genreContainer.Items.GetItemIterator<GenresModel>(maxItemCount: 1);
while (setIterator.HasMoreResults)
{
foreach (GenresModel item in await setIterator.FetchNextSetAsync())
{
genres.Add(item);
}
}
return genres;
}
private static async Task<CosmosContainer> GetOrCreateContainerAsync(CosmosDatabase database, string containerId)
{
CosmosContainerSettings containerDefinition = new CosmosContainerSettings(id: containerId, partitionKeyPath: "/_id");
return await database.Containers.CreateContainerIfNotExistsAsync(
containerSettings: containerDefinition,
throughput: 400);
}
}
}

Dynamic change Microsoft BOT Framework Form's Field Step: hiding and showing the confirmation

I'm new to MS BOT Framework.
I changed MS github MultiDialogsBot.sln , I added a HotelsQuery property to init Form's Field value at HotelsDialog.cs,
public class HotelsDialog : IDialog<object>
{
public HotelsQuery _HotelsQuery { get; set; }
public HotelsDialog()
{
_HotelsQuery = new HotelsQuery{
Destination = "Taiwan",
CheckIn = new DateTime(2017,10,29),
Nights = 3
};
}
public async Task StartAsync(IDialogContext context)
{
await context.PostAsync("Welcome to the Hotels finder!");
var hotelsFormDialog = FormDialog.FromForm(this.BuildHotelsForm, FormOptions.PromptInStart);
context.Call(hotelsFormDialog, this.ResumeAfterHotelsFormDialog);
}
public IForm<HotelsQuery> BuildHotelsForm()
{
OnCompletionAsyncDelegate<HotelsQuery> processHotelsSearch = async (context, state) =>
{
await context.PostAsync($"Ok. Searching for Hotels in {state.Destination} from {state.CheckIn.ToString("MM/dd")} to {state.CheckIn.AddDays(state.Nights).ToString("MM/dd")}...");
};
var destField = new FieldReflector<HotelsQuery>(nameof(HotelsQuery.Destination))
.SetActive((state) =>
{
//depend on _HotelsQuery's values
bool isActive = string.IsNullOrWhiteSpace(_HotelsQuery.Destination);
if (!isActive) state.Destination = _HotelsQuery.Destination;
return isActive;
});
var checkInField = new FieldReflector<HotelsQuery>(nameof(HotelsQuery.CheckIn))
.SetActive((state) =>
{
//depend on _HotelsQuery's values
bool isActive = _HotelsQuery.CheckIn == DateTime.MinValue;
if (!isActive) state.CheckIn = _HotelsQuery.CheckIn;
return isActive;
});
var nightsField = new FieldReflector<HotelsQuery>(nameof(HotelsQuery.Nights))
.SetActive((state) =>
{
//depend on _HotelsQuery's values
bool isActive = _HotelsQuery.Nights == 0;
if (!isActive) state.Nights = _HotelsQuery.Nights;
return isActive;
});
var form = new FormBuilder<HotelsQuery>()
.Field(destField)
.Message("Looking for hotels in {Destination}...")
.Field(checkInField)
.Message("Check in {CheckIn}...")
.Field(nightsField)
.Message("Nights : {Nights}...")
.Confirm("Is this your selection?\n {*}", state =>
{
//clean all fields for showing fields in confirmation
_HotelsQuery.Destination = string.Empty;
_HotelsQuery.CheckIn = DateTime.MinValue;
_HotelsQuery.Nights = 0;
return true;
}, new List<string>())
.Message("Thanks you ...")
.OnCompletion(processHotelsSearch)
.Build();
return form;
}
public async Task ResumeAfterHotelsFormDialog(IDialogContext context, IAwaitable<HotelsQuery> result)
{
try
{
var searchQuery = await result;
var hotels = await this.GetHotelsAsync(searchQuery);
await context.PostAsync($"I found in total {hotels.Count()} hotels for your dates:");
var resultMessage = context.MakeMessage();
resultMessage.AttachmentLayout = AttachmentLayoutTypes.Carousel;
resultMessage.Attachments = new List<Attachment>();
foreach (var hotel in hotels)
{
HeroCard heroCard = new HeroCard()
{
Title = hotel.Name,
Subtitle = $"{hotel.Rating} starts. {hotel.NumberOfReviews} reviews. From ${hotel.PriceStarting} per night.",
Images = new List<CardImage>()
{
new CardImage() { Url = hotel.Image }
},
Buttons = new List<CardAction>()
{
new CardAction()
{
Title = "More details",
Type = ActionTypes.OpenUrl,
Value = $"https://www.bing.com/search?q=hotels+in+" + HttpUtility.UrlEncode(hotel.Location)
}
}
};
resultMessage.Attachments.Add(heroCard.ToAttachment());
}
await context.PostAsync(resultMessage);
}
catch (FormCanceledException ex)
{
string reply;
if (ex.InnerException == null)
{
reply = "You have canceled the operation. Quitting from the HotelsDialog";
}
else
{
reply = $"Oops! Something went wrong :( Technical Details: {ex.InnerException.Message}";
}
await context.PostAsync(reply);
}
finally
{
context.Done<object>(null);
}
}
private async Task<IEnumerable<Hotel>> GetHotelsAsync(HotelsQuery searchQuery)
{
var hotels = new List<Hotel>();
// Filling the hotels results manually just for demo purposes
for (int i = 1; i <= 5; i++)
{
var random = new Random(i);
Hotel hotel = new Hotel()
{
Name = $"{searchQuery.Destination} Hotel {i}",
Location = searchQuery.Destination,
Rating = random.Next(1, 5),
NumberOfReviews = random.Next(0, 5000),
PriceStarting = random.Next(80, 450),
Image = $"https://placeholdit.imgix.net/~text?txtsize=35&txt=Hotel+{i}&w=500&h=260"
};
hotels.Add(hotel);
}
hotels.Sort((h1, h2) => h1.PriceStarting.CompareTo(h2.PriceStarting));
return hotels;
}
}
I have trouble after the confirmation shows. When a user answers yes, BOT will ask CheckIn's prompt.
Why does it not go to the OnCompletion event?
Thanks for your help.
You are clearing out the values in the .Confirm
Try something like this:
var form = new FormBuilder<HotelsQuery>()
.Field(destField)
.Message("Looking for hotels in {Destination}...")
.Field(checkInField)
.Message("Check in {CheckIn}...")
.Field(nightsField)
.Message("Nights : {Nights}...")
.Confirm("Is this your selection?\n {*}", state =>
{
if (_HotelsQuery.Destination == string.Empty ||
_HotelsQuery.CheckIn == DateTime.MinValue ||
_HotelsQuery.Nights == 0)
return false;
return true;
}, new List<string>())
.Message("Thanks you ...")
.OnCompletion(processHotelsSearch)
.Build();

SqlWorkflowInstanceStore WaitForEvents returns HasRunnableWorkflowEvent but LoadRunnableInstance fails

Dears
Please help me with restoring delayed (and persisted) workflows.
I'm trying to check on self-hosted workflow store is there any instance that was delayed and can be resumed. For testing purposes I've created dummy activity that is delayed and it persists on delay.
generally resume process looks like:
get WF definition
configure sql instance store
call WaitForEvents
is there event with HasRunnableWorkflowEvent.Value name and if it is
create WorkflowApplication object and execute LoadRunnableInstance method
it works fine if store is created|initialized, WaitForEvents is called, store is closed. In such case store reads all available workflows from persisted DB and throws timeout exception if there is no workflows available to resume.
The problem happens if store is created and loop is started only for WaitForEvents (the same thing happens with BeginWaitForEvents). In such case it reads all available workflows from DB (with proper IDs) but then instead of timeout exception it is going to read one more instance (I know exactly how many workflows is there ready to be resumed because using separate test database). But fails to read and throws InstanceNotReadyException. In catch I'm checking workflowApplication.Id, but it was not saved with my test before.
I've tried to run on new (empty) persistent database and result is the same :(
This code fails:
using (var storeWrapper = new StoreWrapper(wf, connStr))
for (int q = 0; q < 5; q++)
{
var id = Resume(storeWrapper); // InstanceNotReadyException here when all activities is resumed
But this one works as expected:
for (int q = 0; q < 5; q++)
using (var storeWrapper = new StoreWrapper(wf, connStr))
{
var id = Resume(storeWrapper); // timeout exception here or beginWaitForEvents continues to wait
What is a best solution in such case? Add empty catch for InstanceNotReadyException and ignore it?
Here are my tests
const int delayTime = 15;
string connStr = "Server=db;Database=AppFabricDb_Test;Integrated Security=True;";
[TestMethod]
public void PersistOneOnIdleAndResume()
{
var wf = GetDelayActivity();
using (var storeWrapper = new StoreWrapper(wf, connStr))
{
var id = CreateAndRun(storeWrapper);
Trace.WriteLine(string.Format("done {0}", id));
}
using (var storeWrapper = new StoreWrapper(wf, connStr))
for (int q = 0; q < 5; q++)
{
var id = Resume(storeWrapper);
Trace.WriteLine(string.Format("resumed {0}", id));
}
}
Activity GetDelayActivity(string addName = "")
{
var name = new Variable<string>(string.Format("incr{0}", addName));
Activity wf = new Sequence
{
DisplayName = "testDelayActivity",
Variables = { name, new Variable<string>("CustomDataContext") },
Activities =
{
new WriteLine
{
Text = string.Format("before delay {0}", delayTime)
},
new Delay
{
Duration = new InArgument<TimeSpan>(new TimeSpan(0, 0, delayTime))
},
new WriteLine
{
Text = "after delay"
}
}
};
return wf;
}
Guid CreateAndRun(StoreWrapper sw)
{
var idleEvent = new AutoResetEvent(false);
var wfApp = sw.GetApplication();
wfApp.Idle = e => idleEvent.Set();
wfApp.Aborted = e => idleEvent.Set();
wfApp.Completed = e => idleEvent.Set();
wfApp.Run();
idleEvent.WaitOne(40 * 1000);
var res = wfApp.Id;
wfApp.Unload();
return res;
}
Guid Resume(StoreWrapper sw)
{
var res = Guid.Empty;
var events = sw.GetStore().WaitForEvents(sw.Handle, new TimeSpan(0, 0, delayTime));
if (events.Any(e => e.Equals(HasRunnableWorkflowEvent.Value)))
{
var idleEvent = new AutoResetEvent(false);
var obj = sw.GetApplication();
try
{
obj.LoadRunnableInstance(); //instancenotready here if the same store has read all instances from DB and no delayed left
obj.Idle = e => idleEvent.Set();
obj.Completed = e => idleEvent.Set();
obj.Run();
idleEvent.WaitOne(40 * 1000);
res = obj.Id;
obj.Unload();
}
catch (InstanceNotReadyException)
{
Trace.TraceError("failed to resume {0} {1} {2}", obj.Id
, obj.DefinitionIdentity == null ? null : obj.DefinitionIdentity.Name
, obj.DefinitionIdentity == null ? null : obj.DefinitionIdentity.Version);
foreach (var e in events)
{
Trace.TraceWarning("event {0}", e.Name);
}
throw;
}
}
return res;
}
Here is store wrapper definition I'm using for test:
public class StoreWrapper : IDisposable
{
Activity WfDefinition { get; set; }
public static readonly XName WorkflowHostTypePropertyName = XNamespace.Get("urn:schemas-microsoft-com:System.Activities/4.0/properties").GetName("WorkflowHostType");
public StoreWrapper(Activity wfDefinition, string connectionStr)
{
_store = new SqlWorkflowInstanceStore(connectionStr);
HostTypeName = XName.Get(wfDefinition.DisplayName, "ttt.workflow");
WfDefinition = wfDefinition;
}
SqlWorkflowInstanceStore _store;
public SqlWorkflowInstanceStore GetStore()
{
if (Handle == null)
{
InitStore(_store, WfDefinition);
Handle = _store.CreateInstanceHandle();
var view = _store.Execute(Handle, new CreateWorkflowOwnerCommand
{
InstanceOwnerMetadata = { { WorkflowHostTypePropertyName, new InstanceValue(HostTypeName) } }
}, TimeSpan.FromSeconds(30));
_store.DefaultInstanceOwner = view.InstanceOwner;
//Trace.WriteLine(string.Format("{0} owns {1}", view.InstanceOwner.InstanceOwnerId, HostTypeName));
}
return _store;
}
protected virtual void InitStore(SqlWorkflowInstanceStore store, Activity wfDefinition)
{
}
public InstanceHandle Handle { get; protected set; }
XName HostTypeName { get; set; }
public void Dispose()
{
if (Handle != null)
{
var deleteOwner = new DeleteWorkflowOwnerCommand();
//Trace.WriteLine(string.Format("{0} frees {1}", Store.DefaultInstanceOwner.InstanceOwnerId, HostTypeName));
_store.Execute(Handle, deleteOwner, TimeSpan.FromSeconds(30));
Handle.Free();
Handle = null;
_store = null;
}
}
public WorkflowApplication GetApplication()
{
var wfApp = new WorkflowApplication(WfDefinition);
wfApp.InstanceStore = GetStore();
wfApp.PersistableIdle = e => PersistableIdleAction.Persist;
Dictionary<XName, object> wfScope = new Dictionary<XName, object> { { WorkflowHostTypePropertyName, HostTypeName } };
wfApp.AddInitialInstanceValues(wfScope);
return wfApp;
}
}
I'm not workflow foundation expert so my answer is based on the official examples from Microsoft. The first one is WF4 host resumes delayed workflow (CSWF4LongRunningHost) and the second is Microsoft.Samples.AbsoluteDelay. In both samples you will find a code similar to yours i.e.:
try
{
wfApp.LoadRunnableInstance();
...
}
catch (InstanceNotReadyException)
{
//Some logging
}
Taking this into account the answer is that you are right and the empty catch for InstanceNotReadyException is a good solution.

Categories