7

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.

Tan Sang
  • 1,897
  • 1
  • 16
  • 28

3 Answers3

7

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.

Tan Sang
  • 1,897
  • 1
  • 16
  • 28
-1

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);
Asherguru
  • 1,687
  • 1
  • 5
  • 10
-1

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.

krishg
  • 5,935
  • 2
  • 12
  • 19
  • `partition key is not required since you are reading by ID` => Does it impact to RU and performance when we perform query without partition key? – Tan Sang Aug 26 '20 at 14:34
  • `it's a tricky situation since your predicate might be a condition on any property(ies) of the entity` => I wonder why CosmosDb engineer not support for detect partition key automatic when execute query with partition key in `where` – Tan Sang Aug 26 '20 at 14:39
  • for read by ID, it won't impact RU and performance – krishg Aug 26 '20 at 14:53
  • regarding the second question, the query part in the where condition might have any field of the entity and value (right part of the query). If you do not pass the partition key (as a value, not the field name), how could that be automatically detected? Value can be anything. – krishg Aug 26 '20 at 14:57
  • also if your id itself is the partition key, you can anyway pass that as partition id itself in your FindByIdAsync method to be clean. – krishg Aug 26 '20 at 15:04
  • I am trying to make my project independent on database. But seems like it is imposible, thank you for your reply. – Tan Sang Aug 26 '20 at 18:12
  • Btw, Do you have any recommend about document or source code example for design and building application with Cosmos Db. The MS documeny seems very general to me. Thank you in advance – Tan Sang Aug 26 '20 at 18:22
  • I am not exactly sure about the type of application doc/example you are looking for. Your one stop home shop should be https://learn.microsoft.com/en-us/azure/cosmos-db/. And here is one example for asp.net core app https://learn.microsoft.com/en-us/azure/cosmos-db/sql-api-dotnet-application . You can also check some common use cases https://learn.microsoft.com/en-us/azure/cosmos-db/use-cases .... – krishg Aug 26 '20 at 20:00
  • Sorry, but seems like you are talking about something you are not sure. I already check your recommend but it didn't work. https://imgur.com/a/nVZdZ65 – Tan Sang Aug 28 '20 at 04:44
  • Is it about the PartitionKey.None part? – krishg Aug 28 '20 at 04:51
  • Also, regarding SQL query, it is expected to consume more RU in cross partition query – krishg Aug 28 '20 at 04:54
  • yes, it is. `for read by ID, it won't impact RU and performance ` -> I using sql query for query by id because I can't using ReadItemAsync with PartitionKey.None with your recommend, Did you check it? – Tan Sang Aug 28 '20 at 05:00
  • If you can provide something like document or evidence that you can query by id with PartitionKey.None with the document that have partition key. I will re upvote your answer – Tan Sang Aug 28 '20 at 05:03
  • Ok, PartitionKey.None is for non-partitioned collection (old). Sorry about that. But what about `ReadItemAsync(id, new PartitionKey(id))` ? – krishg Aug 28 '20 at 05:04
  • It only work for container that you set id as partition key itself (of course) – Tan Sang Aug 28 '20 at 05:07
  • also, now updated the answer to remove the PartitonKey.None part (it was miss on my side). You can revisit the answer now. – krishg Aug 28 '20 at 05:14
  • @tấn-sang , wanted to check if you have any update on this. – krishg Sep 05 '20 at 16:49
  • hm, in v2 SDK, partition key is required too...? – Tan Sang Sep 07 '20 at 08:27
  • I've update my answer for this question, could you give me some advise? – Tan Sang Sep 11 '20 at 07:19