Monday 5 January 2015

Step one - A simple Elastic Search Repository using NEST

So, I now have Elastic Search set up on my Mac and decided to build an API using .Net and Nancy that will serve as an inventory service for the cars in my Car Rental-solution. I've made sure I can call the Elastic Search instance using the address mac.localhost:9200. 

The first thing I want to do is to create a kind of repository wrapping Elastic Search, and tests to make sure everything works. But since I really wanna do my solutions teeny tiny, I don't want to do that in a separate project but in the Nancy App itself. Which makes the first step creating the Nancy App that will hold the API.

Nowadays, Nancy comes with some nice project templates for Visual Studio. Just go into Tools >> Extensions and updates to download them.








And now you find them together with all the normal Project templates:













I'm trying hard to do things as simple as possible to start with, which for instance means I try to avoid interfaces unless I see the clear need, meaning either several implementations of a common contract or the need to mock a dependency for unit testing. Here, I see the need to do the latter so I start with a simple ICarRepository interface:

public interface ICarRepository
{
  string Save(Entities.Car car);
        Entities.Car Get(Guid id);
        bool Delete(Guid id);
        IEnumerable<Entities.Car> List();
        IEnumerable<Entities.Car> Search(string query);
}
And as you see, not a trace of generics. Cause this is a small API serving only cars. :)
Now, next step is to implement this contract in an ElasticSearchCarRepository. And to do that, I need an Elastic Search client for .Net.


NEST

The client I've chosen to use is NEST, which seems to have all the basic functionality I could possible want for a simple application. It's one of the two official ES clients, the other being the more low level Elasticsearch.net, which NEST actually uses internally. NEST has a nice strongly typed Query DSL which makes queries against Elastic Search quite easy. To use NEST, just install the Nuget package.
All that is needed to start indexing and querying Elastic Search with NEST is to create an ElasticClient and start using it as the simple examples below show. The "cars"-string is the name of the index targeted by the client.

elasticClient = new ElasticClient(new ConnectionSettings(
 new Uri("http://mac.localhost:9200"), "cars"));
elasticClient.Index<Entities.Car>(car);
elasticClient.Search<Entities.Car>(s => s.QueryString(query))

Since the client is threadsafe I choose to inject it into the repository. Doing that lets me change the client for the tests I'll write so I can test the repository using a testindex that I can play with any way I want to without worrying about destroying real data.

The ElasticSearchCarRepository

The first version of the repository ends up looking like this.

public class ElasticSearchCarRepository : ICarRepository
{
    private readonly ElasticClient _elasticClient;
 
    public ElasticSearchCarRepository(ElasticClient elasticClient)
    {
        _elasticClient = elasticClient;
    }
 
    public string Save(Entities.Car car)
    {
        var result = _elasticClient.Index(car);
        return result.Id;
    }
 
    public Entities.Car Get(Guid id)
    {
        var result = _elasticClient.Get<Entities.Car>(id.ToString());
        return result.Source;
    }
 
    public bool Delete(Guid id)
    {
        var result = _elasticClient.Delete<Entities.Car>(id.ToString(),
            x => x.Type("car"));
        return result.Found;
    }
 
    public IEnumerable<Entities.Car> List()
    {
        var result = _elasticClient.Search<Entities.Car>(search => 
            search.MatchAll());
 
        return result.Documents;
    }
 
    public IEnumerable<Entities.Car> Search(string query)
    {
        var result = _elasticClient.Search<Entities.Car>(search => 
            search.QueryString(query));
 
        return result.Documents;
    }
} 

The only thing really interesting here is the id. Elastic Search creates it's own string id if you don't provide it with a specific id when indexing documents. My car entity already has a Guid id that I want to use. This is possible with a simple mapping in the entity class:

[ElasticType(IdProperty = "CarId")]
public class Car
{
 
    [ElasticProperty(Index = FieldIndexOption.NotAnalyzed)]
    public Guid CarId { getset; }
    public string Make { getset; }
    public CarType CarType { getset; }
    public int DoorCount { getset; }
    public int BagCount { getset; }
    public bool HasAirCondition { getset; }
    public TransmissionType TransmissionType { getset; }
    public decimal RentalPricePerDay { getset; }
    public string Currency { getset; }
 
}

The IdProperty on the class tells Elastic Search which property to use as id. And since this is a Guid, we want to make sure ES does not analyze the field, in which case it would split the guid into several strings using the dash as a delimiter.

Another thing worth mentioning here is that I use two enums. NEST sends these values to ES as the integer values. It works well and the enum gets nicely set again when serializing documents, but I'd rather have the string value in ES for those cases when I just wanna look at the data directly. To do this, I have to set the proper converter when creating the client, like so:

public static ElasticClient CreateElasticClient()
{
    var uri = new Uri(ConfigurationManager.ConnectionStrings
        ["ElasticSearch"].ConnectionString);
    var settings = new ConnectionSettings(uri, "cars");
    settings.AddContractJsonConverters(type => 
        typeof(Enum).IsAssignableFrom(type)
            ? new StringEnumConverter()
            : null);
    return new ElasticClient(settings);
}

Testing the repository

To test the repository out I create a simple class library project and import xUnit.Net as a Nuget. There are some different options on running xUnit-tests. I use Resharper, where the option is available as an extension. Other runners are available via Nuget.

xUnit has a nice and easy way to share context between methods, meaning setup and teardown methods. By creating a fixture-class that inherits from IDisposable, you can put your setup in the constructor and your teardown in the Dispose-method. What I do here is that I create the repository in the fixture-class, using a client targeting a "cars_test"-index. In the Dispose-method, I just delete that index. It's only there for testing so it's ok.

public class ElasticSearchFixtureIDisposable
{
 
    private readonly ElasticSearchCarRepository _repository;
    private readonly ElasticClient _elasticClient;
    private const string IndexName = "cars_test";
 
    public ElasticSearchCarRepository Repository
    {
        get { return _repository;}
    }
 
    public ElasticSearchFixture()
    {
        _elasticClient = CreateElasticClient();
        CreateTestIndex();
        _repository = new ElasticSearchCarRepository(_elasticClient);
    }
 
    private ElasticClient CreateElasticClient()
    {
        var uri = new Uri(ConfigurationManager.ConnectionStrings
            ["ElasticSearch"].ConnectionString);
        var settings = new ConnectionSettings(uri, IndexName);
        settings.AddContractJsonConverters(type => 
            typeof(Enum).IsAssignableFrom(type)
                ? new StringEnumConverter()
                : null);
        return new ElasticClient(settings);
    }
 
    private void CreateTestIndex()
    {
        DeleteIndex();
        _elasticClient.CreateIndex(IndexName, x => x
            .NumberOfReplicas(1)
            .NumberOfShards(1));
    }
 
    
    public void Dispose()
    {
        DeleteIndex();
    }
 
    private void DeleteIndex()
    {
        _elasticClient.DeleteIndex(IndexName);
    }
}

With the fixture in place, I create my test class and can now test if my favourite car can be saved. The fixture class gives me access to the repository and takes care of cleaning up the test data afterwards.

public class ElasticSearchCarRepositoryTests : 
    IClassFixture<ElasticSearchFixture>
{
 
    private readonly ElasticSearchFixture _fixture;
 
    public ElasticSearchCarRepositoryTests(ElasticSearchFixture fixture)
    {
        _fixture = fixture;
    }
 
    [Fact]
    public void Save_Should_Return_Id_Of_Indexed_Car()
    {
        // given
        var carId = Guid.NewGuid();
        var car = CarBuilder.CreateCar()
            .WithId(carId)
            .WithMake("Ford Focus")
            .WithCarType(CarType.Premium).Build();
 
        // when 
        var result = _fixture.Repository.Save(car);
 
        // then
        result.Should().Be(carId.ToString());
    }
}

Very easy first step. With loads of stuff missing. Error handling, logging will come later. But next step is to get the Nancy API up and running with tests. And make everything async.

Code, in one state or another, can be found at github.com: https://github.com/asalilje/CarAPI :)

1 comment:

  1. Thanks Åsa! Got a perfect start to my elastic testing.

    ReplyDelete