Search code examples
c#design-patternsrepository-pattern

Repository Interface with (or without) IProgress


I've got a repository interface (simplified example code):

public interface IPersonRepository
{
    Task<PersonDTO> Get();
}

With two implementations.

One for a direct connection to a database:

public SqlPersonRepository : SqlRepository, IPersonRepository
{
    public SqlPersonRepository(IDbConnectionProvider dbCon) : base(dbCon) { }

    public async Task<PersonDTO> Get()
    {
        // use dbCon and dapper to get PersonDTO from database
    }
}

And another one for remote access via web api:

public ApiPersonRepository : ApiRepository, IPersonRepository
{
    public ApiPersonRepository(IApiConnectionProvider apiCon) : base(apiCon) { }

    public async Task<PersonDTO> Get()
    {
        // use apiCon (contains base url and access token) to perform an HTTP GET request
    }
}

The interface makes sense here, because the server can use the SqlPersonRepository. And the remote (native) client can use the ApiPersonRepository. And for most all of the the use cases, this is all I need.

However, my application supports an extraction of a subset of data from the server so that the client application can run while offline. In this case, I'm not just grabbing one person, I'm grabbing a large set of data (several to tens of megabytes) which many times will be downloaded over a slow mobile connection. I need to pass in an IProgress implementation so I can report progress.

In those cases, I need an ApiDatabaseRepository that looks like this:

public ApiDatabaseRepository : ApiRepository, IDatabaseRepository
{
    public ApiDatabaseRepository(IApiConnectionProvider apiCon) : base(apiCon) { }

    public async Task<DatabaseDTO> Get(IProgress<int?> progress)
    {
        // use apiCon (contains base url and access token) to perform an HTTP GET request
        // as data is pulled down, report back a percent downloaded, e.g.
        progress.Report(percentDownloaded);
    }
}

However the SqlDatabaseRepository does NOT need to use IProgress (even if Dapper COULD report progress against a database query, which I don't think it can). Regardless, I'm not worried about progress when querying the database directly, but I am worried about it when making an API call.

So the easy solution, is that the SqlDatabaseRepository implementation accepts the IProgress parameter, with a default value of null, and then the implementing method just ignores that value.

public SqlDatabaseRepository : SqlRepository, IDatabaseRepository
{
    public SqlDatabaseRepository(IDbConnectionProvider dbCon) : base(dbCon) { }

    public async Task<DatabaseDTO> Get(IProgress<int?> progress = null)
    {
        // use dbCon and dapper to get DatabaseDTO from database
        // progress is never used
    }
}

But that smells funny. And when things smell funny, I wonder if I'm doing something wrong. This method signature would give the indication that progress will be reported, even though it won't.

Is there a design pattern or a different architecture I should be using in this case?


Solution

  • Oversimplifying this, you basically have 2 options: having a consistent interface or not.

    There are, of course other design patterns which might work here, (e.g.; some decorators and a factory method), but I believe them to be overkill.

    If you stick to the general rule that consistent interface is desired, I think having a "not entirely implemented" callback technique isn't that bad. You could also consider just to implement it - or at least: make it return something which makes sense.

    I would definitely avoid a construction with 2 different interfaces of some kind. Although sometimes this is the better option (when checking if something supports something), e.g.; testing if a hardware component is available - I see it as overkill in your scenario. It would also put more logic at the caller side, and unless you want to open a process-dialog screen only in this scenario, I would avoid it.

    A last note: there are alternative progress report patterns such as using an event, or, passing in an optional callback method. This latter looks like your solution but is in fact a little different.

    Still this faces you with the same issue in the end, but might be worth to consider.


    There are many more solutions - but given the context you provided, I am not sure if they apply. And keep in mind, this is highly opinion based.