Leron
Leron

Reputation: 9846

Calling multiple async methods within other method without returning result

I am working on an ASP.NET Core 2.2 application with Dapper. Virtually all methods follows the async/await pattern returning some sort of Task as a result.

Due to an issue we had with Dapper (I haven't personally investigated it, this part of the code I use as it is) but basically it boils down to the fact that if you want to execute in Transaction multiple async methods, which internally are calling other async methods and you may have several levels of nesting this way, you need to wrap all those method invocations within a single method which would be executed in a transaction.

The method that handles this is as follows:

public async Task<TOut> ExecuteInTransactionAsync<TOut>(
    Delegate function, 
    params object[] parameters)
{
    using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
    {
        var result = await ((Task<TOut>)function.DynamicInvoke(parameters))
            .ConfigureAwait(false);

        scope.Complete();

        return result;
    }
}

So I have a very complex Entity which is saved by calling Save to a lot of smaller entities. This part of the code is working OK and looks like this:

public async SaveEntireEntity(EntityDTO entityDTO)
{
  return await _transactionProvider.ExecuteInTransactionAsync<dynamic>(
     new Func<object, Task<dynamic>>(async dto => await SaveInTransaction(dto)),
     new { Name = entityDTO.Name, Address = entityDTO.Address, Age = entityDTO.Age });
}

And the SaveInTransaction method looks like:

private async Task<dynamic> SaveInTransaction(dynamic dto)
{
  var entityId = await nameService.Add(dto.Name);
  await addressService.Add(dto.Address);
  await ageService.Add(dto.Age);

  return entityId;
}

so this is simplified, but indeed I am calling a multiple services here, which on their part are calling multiple repositories and this works fine.

The Problem I have is when it comes to updating the same entity within a transaction. The whole purpose of showing the Save logic was to point out that at the end because I have this return entityId; I am able to chain everything together without any problems. However, as it is right now, by default our Update methods are not returning anything and this is where I can't figure out how to implement the Update logic.

Currently I have this:

public async Task UpdateEntireEntity(UpdateEntityDTO, entityDTO)
{
  await _transactionProvider.ExecuteInTransactionAsync<dynamic>(
    new Func<object, Task<dynamic>>(async dto => await UpdateInTransaction(dto)),
    new { Name = entityDTO.Name, Address = entityDTO.Address, Age = entityDTO.Age });
}

And UpdateInTransaction looks like this:

private async Task<dynamic> UpdateInTransaction(dynamic dto)
{
  await UpdateName(dto.Name);
  await UpdateAddress(dto.Address);
  await UpdateAge(dto.Age);

  return await Task.FromResult<dynamic>(null);
}

This seems to work at least based on the several tests I made, however I really don't like this part:

return await Task.FromResult<dynamic>(null);

To me it seems like an ugly hack. The Update methods were thought not to return any value and this is just too artificial.

And even the worst part is that I can not figure out how implement the update method without having to return something.

One thing I've tried is to change the declaration of UpdateInTransaction to

private async Task UpdateInTransaction(dynamic dto)

and when I call the method I change it to:

await _transactionProvider.ExecuteInTransactionAsync<dynamic>(
new Func<object, Task>( async dto => await UpdateInTransaction(dto)..

But I got the following exception:

AsyncStateMachineBox1[System.Threading.Tasks.VoidTaskResult, <fully-qualified- name>.<<UpdateEntireEntity>b__0>d] to type 'System.Threading.Tasks.Task1[System.Threading.Tasks.Task]'

. So basically that's it. Sorry for the long post. I would really appreciate some well explained answer.

Upvotes: 1

Views: 1429

Answers (2)

Stephen Cleary
Stephen Cleary

Reputation: 456322

I would avoid the use of Delegate since it isn't typed:

public async Task<TOut> ExecuteInTransactionAsync<TOut>(Func<Task<TOut>> function)
{
    using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
    {
        var result = await (function()).ConfigureAwait(false);

        scope.Complete();

        return result;
    }
}

This signature would mean you'd need to capture parameters rather than pass them:

public async Task<dynamic> SaveEntireEntity(EntityDTO entityDTO)
{
  return await _transactionProvider.ExecuteInTransactionAsync(
     async () => await SaveInTransaction(
         new { Name = entityDTO.Name, Address = entityDTO.Address, Age = entityDTO.Age }));
}

Once you're using the strongly-typed Func<Task<T>> instead of Delegate in your method signature, you can create an overload for ExecuteInTransactionAsync as such:

public async Task ExecuteInTransactionAsync(Func<Task> function)
{
    using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
    {
        await (function()).ConfigureAwait(false);
        scope.Complete();
    }
}

which can be used as such:

public async Task UpdateEntireEntity(UpdateEntityDTO entityDTO)
{
  await _transactionProvider.ExecuteInTransactionAsync(
    async () => await UpdateInTransaction(
        new { Name = entityDTO.Name, Address = entityDTO.Address, Age = entityDTO.Age }));
}

private async Task UpdateInTransaction(dynamic dto)
{
  await UpdateName(dto.Name);
  await UpdateAddress(dto.Address);
  await UpdateAge(dto.Age);
}

Upvotes: 4

FlyingFoX
FlyingFoX

Reputation: 3509

You can change

private async Task<dynamic> UpdateInTransaction(dynamic dto)
{
  await UpdateName(dto.Name);
  await UpdateAddress(dto.Address);
  await UpdateAge(dto.Age);

  return await Task.FromResult<dynamic>(null);
}

to

private async Task<dynamic> UpdateInTransaction(dynamic dto)
{
  await UpdateName(dto.Name);
  await UpdateAddress(dto.Address);
  await UpdateAge(dto.Age);

  return null;
}

Upvotes: 0

Related Questions