Repository that support query by partition key without change interface

Viewed 501

I am developing an application that using IDocumentClient to perform query to CosmosDB. My GenericRepository support for query by Id and Predicate.

I am in trouble when change Database from SqlServer to CosmosDb, in CosmosDb, we have partition key. And I have no idea how to implement repository that support query by partition key without change interface to pass partition key as a argument.

public interface IRepository<T>
{
    //I can handle this one by adding value of partition key to id and split it by ":"
    Task<T> FindByIdAsync(string id);

    // I am stuck here!!!
    Task<T> FindByPredicateAsync(Expression<Func<T, bool>> predicate);
}

My implementation

public class Repository<T> : IRepository<T>
{
    private readonly IDocumentClient _documentClient;

    private readonly string _databaseId;
    private readonly string _collectionId;

    public Repository(IDocumentClient documentClient, string databaseId, string collectionId)
    {
        _documentClient = documentClient;

        _databaseId = databaseId;
        _collectionId = collectionId;
    }

    public async Task<T> FindByIdAsync(string id)
    {
        var documentUri = UriFactory.CreateDocumentUri(_databaseId, _collectionId, id);

        try
        {
            var result = await _documentClient.ReadDocumentAsync<TDocument>(documentUri, new RequestOptions
            {
                PartitionKey = ParsePartitionKey(documentId)
            });

            return result.Document;
        }
        catch (DocumentClientException e)
        {
            if (e.StatusCode == HttpStatusCode.NotFound)
            {
                throw new EntityNotFoundException();
            }

            throw;
        }
    }
    
    public async Task<T> FindByPredicateAsync(Expression<Func<T, bool>> predicate)
    {
         //Need to query CosmosDb with partition key here!
    }

    private PartitionKey ParsePartitionKey(string entityId) => new PartitionKey(entityId.Split(':')[0]);
}

Any help is greatly appreciated, thanks.

3 Answers

I've found out the solution to keep your repository independent of database(I am using v3 SDK for example). Just separated current interface into 2 parts:

public interface IRepository<T>
{
    Task<T> FindItemByDocumentIdAsync(string documentId);

    
    Task<IEnumerable<T>> FindItemsBySqlTextAsync(string sqlQuery);

    Task<IEnumerable<T>> FindAll(Expression<Func<T, bool>> predicate = null);
}

public interface IPartitionSetter<T>
{
    string PartititonKeyValue { get; }

    void SetPartitionKey<T>(string partitionKey);
}//using factory method or DI framework to create same instance for IRepository<T> and IPartitionSetter<T> in a http request

Implementation:

public class Repository<T> : IRepository<T>, IPartitionSetter<T>
{
    //other implementation

    public async Task<IEnumerable<T>> FindAll(Expression<Func<T, bool>> predicate = null)
    {
        var result = new List<T>();
        var queryOptions = new QueryRequestOptions
        {
            MaxConcurrency = -1,
            PartitionKey = ParsePartitionKey()
        };

        IQueryable<T> query = _container.GetItemLinqQueryable<T>(requestOptions: queryOptions);

        if (predicate != null)
        {
            query = query.Where(predicate);
        }

        var setIterator = query.ToFeedIterator();
        while (setIterator.HasMoreResults)
        {
            var executer = await setIterator.ReadNextAsync();

            result.AddRange(executer.Resource);
        }

        return result;
    }

    private string _partitionKey;

    public string PartititonKeyValue => _partitionKey;

    private PartitionKey? ParsePartitionKey()
    {
        if (_partitionKey == null)
            return null;
        else if (_partitionKey == string.Empty)
            return PartitionKey.None;//for query documents with partition key is empty
        else
            return new PartitionKey(_partitionKey);
    }

    public void SetPartitionKey<T>(string partitionKey)
    {
        _partitionKey = partitionKey;
    }
}

You will need to inject IPartitionSetter<T> and call SetPartitionKey before execute query to apply partition key here.

Is this what you want?

BaseModel.cs (not needed. necessary only when you use generic save/update)

public class BaseModel
{
     public int Id { get; set; }
     public DateTime? CreatedDate { get; set; }
     public string CreatedBy { get; set; }
     public DateTime? ModifiedDate { get; set; }
     public string ModifiedBy { get; set; } 
}

User.cs

public class User : BaseModel
{
     public string Name { get; set; }
     public int? Age { get; set; }
}

YourRepository.cs

public Task<T> FindByPredicateAsync(Expression<Func<T, bool>> predicate)
{
     return _context.Set<T>().Where(predicate).FirstOrDefault();
}

YourController.cs

string id = "1:2";
string[] ids = id.Split(":");

Expression<Func<User, bool>> exp = x => ids.Contains(x.Id);
FindByPredicateAsync<User>(exp);

It seems you are trying to use a part of the Document ID as partition key in the FindByIdAsync method. Not sure if I could follow the context behind that logic or it's just random attempt. You can use the document ID itself as the partition key for your container (aka collection) if you really do not have any other property of the entity to be a good partition key.

NOTE: I see you are using older V2 SDK in your sample code above. So I have provided both V2 and the newer V3 SDK example in my answer below in case you still want to stick to V2 for now.

For the documentClient.ReadDocumentAsync (V2 SDK) call, partition key is not required since you are reading by ID (and if your partition key is the id itself). In case of V3 SDK, container.ReadItemAsync, you can pass the id itself as partition key assuming if you chose that as partition key as I mentioned in the beginning.

Now regarding the other method FindByPredicateAsync, it's a tricky situation since your predicate might be a condition on any property(ies) of the entity. If you pass partition key, it will query only within the same partition missing records from other partitions which might match the predicate. Example (V2 SDK) and Example (V3 SDK). So one option is to use cross partition query by setting EnableCrossPartitionQuery property of Request Options to true in case of V2 SDK and do not set partition key. In V3 SDK, if you do not set the partition key of the QueryRequestOptions, it will enable cross partition automatically. CAUTION: Watch out for performance and RU cost implication of cross-partition query.

For easy overall reference, here is Cosmos DB documentation Map.

Related