4
\$\begingroup\$

I'm implementing a number of classes in C# that have async methods. However, each of these methods needs to implement some boilerplate code that is common to all. Therefore I've implemented the common code in an async method of a base class; the async methods on the derived classes then call this base class method.

Here's the base class, containing the method that does the common stuff (GetResultAsync).

namespace Redacted
{
    abstract class RepositoryBase : IRepository
    {
        protected RepositoryBase(IRepositoryConfiguration configuration)
        {
            Configuration = configuration;
        }

        protected IRepositoryConfiguration Configuration { get; private set; }

        protected FleetContext CreateDatabaseContext()
        {
            return new FleetContext(Configuration.DatabaseConnectionString);
        }

        protected async Task<IApiResult<TValue>> GetResultAsync<TValue>(
            IApiCallerContext context,
            Func<FleetContext, CancellationToken, Task<TValue>> getTask)
        {
            using (new SynchronizationContextChange())
            using (FleetContext db = CreateDatabaseContext())
            {
                bool isSystemOnline = await db.IsSystemOnlineAsync();
                if (!isSystemOnline)
                {
                    return ImmutableApiResult<TValue>.Offline;
                }

                bool isValidSession = await db.IsValidSessionAsync(context.Caller);
                if (!isValidSession)
                {
                    return ImmutableApiResult<TValue>.Blocked;
                }

                Task<TValue> task = getTask(db, context.CancellationToken);
                TValue value = await task;

                return ImmutableApiResult.Create<TValue>(value);
            }
        }
    }
}

Basically, I have an Entity Framework database context (FleetContext) that exists for the duration of the call. I then perform some common DB-related tasks by calling custom async methods that have been added to the context class. Depending on the result of these calls, I may exit the method early: in such cases, the IApiResult object returned by the method is different than when the method runs through to the end normally.

Here's an example of a derived class that contains a method which calls the base class:

namespace Redacted
{
    sealed class CompanyRepository : RepositoryBase, ICompanyRepository
    {
        public CompanyRepository(IRepositoryConfiguration configuration)
            : base(configuration)
        {
        }

        public Task<IApiResult<ICompanyIdentity>> GetCompanyIdentityAsync(int companyId, IApiCallerContext context)
        {
            return GetResultAsync<ICompanyIdentity>(
                context,
                async (db, cancellationToken) =>
                {
                    Company company = await db.Companies.FindAsync(cancellationToken, companyId);
                    return ImmutableCompanyIdentity.Create(company);
                }
                );
        }
    }
}

As you can see I'm using an asynchronous anonymous function to return the Task object required by the GetResultAsync method.

This all works but seems somewhat inside-out to me, in that the API method (GetCompanyIdentityAsync) passes a function to the base class, rather than the more typical approach of the derived class calling the base method as part of its normal processing.

Am I worrying unnecessarily, or are there obvious alternate ways of calling the boilerplate code from the derived classes?

\$\endgroup\$
3
  • \$\begingroup\$ I'd say it's questionable whether that method should be in a base class rather than a dependency. \$\endgroup\$ Mar 19, 2015 at 14:15
  • \$\begingroup\$ @BenAaronson Sorry, I don't follow. Are you suggesting that the GetResultAsync method should be in a separate helper class? If so, what would be the reasoning behind that? (These are genuine questions, I'm not trying to pick an argument) \$\endgroup\$ Mar 19, 2015 at 14:58
  • 1
    \$\begingroup\$ Yep, that's what I'm suggesting. Not 100% sure how appropriate it is but generally if picking between composition and inheritance and I don't see a significant immediate advantage to either (as in this case), I'd go with composition. \$\endgroup\$ Mar 19, 2015 at 15:24

1 Answer 1

3
\$\begingroup\$

In your code you create 2 boolean variables for a simple if statement conditional, personally I think that you shouldn't create these variables unless you are going to use them in more than one place or for complex condition statements.

        using (FleetContext db = CreateDatabaseContext())
        {
            bool isSystemOnline = await db.IsSystemOnlineAsync();
            if (!isSystemOnline)
            {
                return ImmutableApiResult<TValue>.Offline;
            }

            bool isValidSession = await db.IsValidSessionAsync(context.Caller);
            if (!isValidSession)
            {
                return ImmutableApiResult<TValue>.Blocked;
            }

            Task<TValue> task = getTask(db, context.CancellationToken);
            TValue value = await task;

            return ImmutableApiResult.Create<TValue>(value);
        }

You can also avoid creating the Task variable at the end as well and just create the value.

using (FleetContext db = CreateDatabaseContext())
{
    if (!(await db.IsSystemOnlineAsync()))
    {
        return ImmutableApiResult<TValue>.Offline;
    }
    else if (!(await db.IsValidSessionAsync(context.Caller)))
    {
        return ImmutableApiResult<TValue>.Blocked;
    }

    TValue value = await (getTask(db, context.CancellationToken));

    return ImmutableApiResult.Create<TValue>(value);
}

This shortens the code a little bit and gets straight to the point. Nothing really wrong with the way that you did it, and I haven't really done anything different here either, I just...well... golfed it a bit I guess. I just didn't think we needed to create all those variables

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.