Repository interfaces

ISearchRepository

Vonk.Core.Repository.ISearchRepository is central in the functioning of Firely Server. It defines all read-access to the underlying repository, being one of Firely Server’s own database implementations or a Facade implementation.

It has a single method:

Task<SearchResult> Search(IArgumentCollection arguments, SearchOptions options);

Using ISearchRepository

  1. Have it injected by the dependency injection into the class where you need it:

    public MyService(ISearchRepository searchRepository)
    {
       _searchRepository = searchRepository;
    }
    

    Note

    Implementations of ISearchRepository have a Scoped lifetime, so MyService should also be registered as Scoped:

    public IServiceCollection AddMyServices(this IServiceCollection services)
    {
       services.TryAddScoped<MyService>();
       return services;
    }
    
  2. Prepare search arguments that express what you are looking for. Search arguments are effectively key-value pairs as if they came from the querystring on the request. So the key must be the code of a supported Searchparameter.

    var args = new ArgumentCollection(new IArgument[]
    {
          new Argument(ArgumentSource.Internal, ArgumentNames.resourceType, "Patient") {MustHandle = true}, // MustHandle = true is optional
          new Argument(ArgumentSource.Internal, "name", "Fred") {MustHandle = true} // MustHandle = true is optional
    }).AddCount(20);
    

    Note

    The Search implementation will in general update the arguments, especially their Status property and the Issue if something went wrong. So be careful with reuse of arguments. Use IArgumentCollection.Clone() if necessary .

  3. Prepare search options that guide the search. Usually you can use one of the predefined options on the SearchOptions class.

    var options = SearchOptions.Latest(vonkContext.ServerBase, vonkContext.Request.Interaction, vonkContext.InformationModel);
    
  4. Execute the search.

    var searchResult = await _searchRepository.Search(args, options);
    
  5. Check the status of the arguments, especially if they could not be ignored (MustHandle = true). Because this is a common pattern, there is an extension method CheckHandled that throws a VonkParameterException if MustHandle arguments are not handled.

    try
    {
       args.CheckHandled("Arguments must all be handled in MyService");
    }
    catch (VonkParameterException vpe)
    {
       //report it in the vonkContext.Response.Outcome
    }
    
  6. Inspect the number of the results to check whether anything was found. If so, you can enumerate the results or process the set as a whole, since SearchResult implements IEnumerable<IResource>.

    if (searchResult.TotalCount > 0)
    {
       foreach(var resource in searchResult)
       { ... }
    }
    

Implement ISearchRepository

Implementing ISearchRepository is only needed in a Facade.

The general pattern for implementing ISearchRepository is:

  1. For each of the IArguments in the IArgumentCollection:

    1. If you support the argument, translate it to a ‘where’ clause on your repository. If your backend is another Web API, this could have the form of a piece of a querystring.

    2. Call IArgument.Handled() to update its status. There is also .Warning() and .Error() when something is wrong with the argument. If you simply don’t support the argument, you can leave the status to ‘Unhandled’.

    3. Pay special attention to the _count and _skip arguments for proper paging.

  2. ‘AND’ all the arguments together, e.g. forming a database query or complete querystring.

  3. Issue the query to your repository and await the results (await used intentionally: this should be done asynchronously).

  4. For each of the resulting records or objects: map them to matching FHIR resources, either by:

    1. Creating POCO’s:

      var result = new Patient() { /*fill in from the source object*/ };
      return result.ToIResource(); //InformationModel implied by the assembly of class Patient
      
    2. or by crafting SourceNodes:

      var result = SourceNode.Resource("Patient", SourceNode.Valued("id", /* id from source */), SourceNode.Node("meta", SourceNode.Valued("versionId", "v1"), ....), ...))
      return result.ToIResource(VonkConstants.Model.FhirR3 /* or FhirR4 */);
      
  5. Combine the mapped resources into a SearchResult:

    return new SearchResult(resources, pagesize, totalCount, skip);
    
    • pagesize: should be the value of the _count argument, unless you changed it for some reason.

    • totalCount: total number of results, if there are more than you are returning right now.

    • skip: number of results skipped in this set (if you are serving page x of y).

For a Facade on a relational database we provide a starting point with Vonk.Facade.Relational.SearchRepository. Follow the exercise in Exercise: Build your first Facade to see how that is done.

IResourceChangeRepository

IResourceChangeRepository defines methods to change resources in the repository:

public interface IResourceChangeRepository
{
   Task<IResource> Create(IResource input);
   Task<IResource> Update(ResourceKey original, IResource update);
   Task<IResource> Delete(ResourceKey toDelete, string informationModel);
   string NewId(string resourceType);
   string NewVersion(string resourceType, string resourceId);
}

ResourceKey is a simple struct to identify a resource by Type, Id and optionally VersionId.

Using IResourceChangeRepository

You should hardly ever need to use the IResourceChangeRepository. It is used by the Create, Update, Delete and the conditional variations thereof.

Should you need to use it, the methods are fairly straightforward.

method:

Create

description:

Provide an IResource having an id and versionId. These can be obtained by calling NewId and NewVersion. The return value will contain a possibly updated IResource, if the implementation changed or added elements. To fill in all the metadata there is a convenient extension method on IResourceChangeRepository:

var withMetaInfo = changeRepository.EnsureMeta(resource); //Will keep existing id, version and lastUpdated and fill in if missing.
//EnsureMeta also exists as extension method on IResource - that uses Guids for id and version.
var createdResource = await changeRepository.Create(withMetaInfo);
method:

Update

description:

Assert that a resource exists that can be updated. If not, use Create, otherwise go for Update.

var existingKey = new ResourceKey(resourceType, resourceId);
var args = existingKey.ToArguments(true);
var args = args.AddCount(0); //We don't need the actual result - just want to know whether it is there.
var options = SearchOptions.Latest(vonkContext.ServerBase, VonkInteraction.type_search, InformationModel: null); //search across informationmodels, we expect ids to be unique.
var exists = (await searchRepository.Search(args, options)).TotalCount = 1; //Take care of < 1 or > 1 matches

resource.EnsureMeta(KeepExisting.Id) //Will keep existing id and provide fresh version and lastUpdated.
var updatedResource = await changeRepository.Update(existingKey, resource);
method:

Delete

description:

Delete the resource that matches the provided key and informationModel. Returns the resource that was deleted.

var existingKey = new ResourceKey(resourceType, resourceId);
var deletedResource = await changeRepository(existingKey, vonkContext.InformationModel);
method:

NewId

description:

Get a new Id value generated by the repository (e.g. when the repository wants to use a sequence generator or ids in a specific format). Generally used through the extension method IResourceChangeRepository.EnsureMeta(IResource resource, KeepExisting keepExisting), see Create above.

method:

NewVersion

description:

Get a new Version value generated by the repository (e.g. when the repository wants to use a sequence generator or ids in a specific format). The repository may want to base the version on the id, therefore the Id is passed as an argument. Generally used through the extension method IResourceChangeRepository.EnsureMeta(IResource resource, KeepExisting keepExisting), see Create above.

Implement IResourceChangeRepository

Implementing IResourceChangeRepository is only needed in a Facade that wants to provide write-access to the underlying repository.

For all three methods, you will have to map data from FHIR resources to your internal data structures and back.

Note that you also need to implement ISearchRepository to support the Create and Update plugins and of course the conditional variants of those.