Polly and Blazor, Part 3 – Dependency Injection

Full source code available here.

This post continues to build on the two previous on using Polly with Blazor. The first was a simple example of using a Wait and Retry while updating the display as the policy retried, the second did the same but made use of the Polly Context. The Polly Context is something I have not made a lot of use of in the past, but I have a feeling that it will be very helpful with Blazor applications.

I’m keeping the examples similar, deliberately changing a single thing in each. In this post I will define the Wait and Retry Policy in the Startup, add it to the service collection and inject it into the Razor page.

One big difference to note is that I can no longer update the display from inside the OnRetry delegate of the Policy. Gone is the call to InvokeAsync(StateHasChanged) because this is dependent on the the ComponentBase.

In Startup.cs add the following, it defines the Wait and Retry policy, and its OnRetry delegate –

public void ConfigureServices(IServiceCollection services)
    {
    IAsyncPolicy<HttpResponseMessage> _httpRequestPolicy = Policy.HandleResult<HttpResponseMessage>(
    r => !r.IsSuccessStatusCode)
    .WaitAndRetryAsync(3,
        retryAttempt => TimeSpan.FromSeconds(retryAttempt), onRetry: (response, retryDelay, retryCount, context) =>
        {
            context["message"] = context["message"] + $"\nReceived: {response.Result.StatusCode}, retryCount: {retryCount}, delaying: {retryDelay.Seconds} seconds";
        });

    services.AddSingleton<IAsyncPolicy<HttpResponseMessage>>(_httpRequestPolicy);
    // snip...

In the Razor section of the Index.razor page add this –

@inject IAsyncPolicy<HttpResponseMessage> _httpRequestPolicy

The policy will now be injected into the Razor page.

Using it is simple, see line 16 below –

@code {
   HttpClient httpClient = new HttpClient() {
        BaseAddress =  new Uri("http://localhost:5000")
    };

    private string status;
    private Context context = new Polly.Context("", new Dictionary<string, object>() {{"message",""}}); 
    private string responseCode;
    private string number;

    private async Task CallRemoteServiceAsync()
    {
        status = "Calling remote service...";
        string requestEndpoint = "faulty/";

        HttpResponseMessage httpResponse = await _httpRequestPolicy.ExecuteAsync(ctx => httpClient.GetAsync(requestEndpoint), context);
        responseCode = httpResponse.StatusCode.ToString();
        number = await httpResponse.Content.ReadAsStringAsync();
        status = "Finished";
    }
}

Instead of injecting a Polly Policy you could inject a Polly Registry with more than one policy. I have written about the registry a few times on this blog.

Full source code available here.

Polly and Blazor, Part 2 – Using the Context

Full source code available here.

This post is a short follow up to the one where I used Polly Wait and Retry in Blazor. In that post I used variables defined in my C# method to pass information back to the calling code, to display on the screen. But here I’m going to show how the Polly Context can be used instead.

The setup and almost all the code is the same, so please take a look at that post.

Using the Context
The Razor section references the message in the Polly context rather than a defined variable for the message –

<p>Polly Summary : @context["message"]</p>

In the code block add a Polly Context and initialize the dictionary with a message, this is important because the Razor block references this message when the page loads.

@code {
    //snip...

    private Context context = new Polly.Context("", new Dictionary<string, object>() {{"message",""}});
    //snip... 

Then in the rest of the code block, update the message in the context with the information to display to the user.

private async Task CallRemoteServiceAsync()
{
    IAsyncPolicy<HttpResponseMessage> _httpRequestPolicy = Policy.HandleResult<HttpResponseMessage>(
        r => !r.IsSuccessStatusCode)
        .WaitAndRetryAsync(3,
            retryAttempt => TimeSpan.FromSeconds(retryAttempt * 2), onRetry: (response, retryDelay, retryCount, context) => {
                context["message"] = $"Recieved: {response.Result.StatusCode}, retryCount: {retryCount}, delaying: {retryDelay.Seconds} seconds\n";
                Console.WriteLine(context["message"]);
                InvokeAsync(StateHasChanged);
    });
    status = "Calling remote service...";
    string requestEndpoint = "faulty/";
    HttpResponseMessage httpResponse = await _httpRequestPolicy.ExecuteAsync(ctx => httpClient.GetAsync(requestEndpoint), context);
    responseCode = httpResponse.StatusCode.ToString();
    number = await httpResponse.Content.ReadAsStringAsync();
    status = "Finished";
    context["message"] = "";
}

That’s it, another way to do the same thing.

In the next post I’ll show how to use a Polly Policy defined within the OnInitializedAsync block.

Full source code available here.

Logging to DataDog with Serliog and .Net 5

Full source code available here.

This is a quick post to showing how to setup a .Net 5 Web Api application with logging to DataDog from your local computer.

Getting Datadog up and running

First step is to create an account with Datadog and then install the Datadog agent on your computer, in my case I am using Ubuntu.

There are a few small configuration steps to get the agent up and running.

In the datadog.yaml file look for an entry called logs_enabled, make sure this is set to true.

##################################
## Log collection Configuration ##
##################################

## @param logs_enabled - boolean - optional - default: false
## Enable Datadog Agent log collection by setting logs_enabled to true.
#
logs_enabled: true

In the /etc/datadog-agent/conf.d/ create a directory called mydotnet5app.d.

Inside that directory create a file called conf.yaml and paste in the below.

#Log section
logs:

    # - type : (mandatory) type of log input source (tcp / udp / file)
    #   port / path : (mandatory) Set port if type is tcp or udp. Set path if type is file
    #   service : (mandatory) name of the service owning the log
    #   source : (mandatory) attribute that defines which integration is sending the log
    #   sourcecategory : (optional) Multiple value attribute. Can be used to refine the source attribute
    #   tags: (optional) add tags to each log collected

  - type: file
    path: /path/to/my/logfile/application.log
    service: csharp
    source: csharp

The .Net 5 Application

I have a simple .Net 5 Web Api application. I added Serilog to log to a file at the location specified in the conf.yaml above.

In Program.cs I add the below. Note the JsonFormatter on line 4, that is important for structured logging in DataDog –

public static void Main(string[] args)
{
    Log.Logger = new LoggerConfiguration()
        .WriteTo.File(new JsonFormatter(renderMessage: true), "webapi.log")
        .CreateLogger();

    try
    {
        Log.Information("Starting up...");
        CreateHostBuilder(args).Build().Run();
    }
    catch (Exception ex)
    {
        Log.Fatal(ex, "Application start-up failed");
    }
    finally
    {
        Log.CloseAndFlush();
    }
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .UseSerilog()
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });
}

That’s all you need to configure logging, all that is left is to add a few logging statements.

Here is a simple controller that lets you do division from, giving the opportunity to generate and log divide by zero exceptions.

[ApiController]
[Route("[controller]")]
public class MathController : ControllerBase
{
    private readonly ILogger<MathController> _logger;

    public MathController(ILogger<MathController> logger)
    {
        _logger = logger;
    }

    [HttpGet("division")]
    public ActionResult Get(int a, int b)
    {
        _logger.LogInformation("Dividing {numerator} by {denominator}", a, b); // note how I can change the names of the attributes that DataDog logs to numerator and denominator.

        int answer;
        try
        {
            answer = a / b;
        }
        catch (DivideByZeroException ex)
        {
            _logger.LogError(ex, "SomeId: {someId}. ErrorCode: {errorCode} Division didn't work, was dividing {numerator} by {denominator}", Guid.NewGuid(), -99, a, b);

            return BadRequest("Dividing by zero is not allowed");
        }
        _logger.LogInformation("Returning {result}", answer); // again the name of the attribute logged is different from the variable name in the code.
        return Ok(answer);
    }
}

You can also log more complex objects in DataDog, like a Person with an id, first name, last name and date of birth. The full code for this is in the attached zip in the PersonController.cs file, but it is as simple as –

public ActionResult Post(Person person)
{
    _logger.LogDebug("Received new person: {@person}",person);

And here is what it looks like in DataDog.

A successful request –

And one that generates an exception –

And one that logs a complex object –

Now, there is a huge variety of things you can do within DataDog to search, alert, monitor, etc on the data but I’m not going into that here.

Full source code available here.

Indexing the Works of Shakespeare in ElasticSearch – Part 4, Searching via Web API in .NET 5

Full source code available here.

This is part four of my four part series on indexing the works of Shakespeare in ElasticSearch.

In this I’ll show how to use the ElasticSearch “low level client” to perform the search. In the past I wrote a blog showing how to use a HttpClient to perform the search using Json, and this works fine, but Steve Gordon suggested I try to the Elastic client as it supports things like connection pooling and still lets me use Json directly with ElasticSearch.

To go along with the Elastic “low level client” there is a “high level client” called NEST, I have tried both and prefer to stick with Json, but you may find them more useful.

Because I develop on a few languages, Json is the natural choice for me. I use it when querying from Node.js, inside a HTTP client (Fiddler, Rest Client, etc) when figuring out my queries and I want to use it in .NET.

But Json and C# don’t go together very well, you have to jump through hoops to make it work with escaping. Or, as I have doe, use a creative approach to deserializing via dynamic objects (I know some people won’t like this), I find this much more convenient than converting my Json queries to the Elastic client syntaxes.

This examples shows how to use the a Web API application to search for a piece of text in isolation or within specific play.

The setup
There is very little to do here.

In Startup.cs add the following to the ConfigureServices(..) method –

services.AddSingleton<ElasticLowLevelClient>(new ElasticLowLevelClient(new ConnectionConfiguration(new Uri("http://localhost:9200"))));

In the SearchController add the following to pass the ElasticSearch client in via dependency injection –

public class SearchController : ControllerBase
{
    private readonly ElasticLowLevelClient _lowLevelClient;
    public SearchController(ElasticLowLevelClient lowLevelClient)
    {
        _lowLevelClient = lowLevelClient;
    }
//snip ..

I have two action methods, one to search for a play and line, and one to search for a line across all plays (I know they could be combined into a single action method, I want keep things simple) –

[HttpGet("Line")]
public ActionResult Line(string text)
{
    string queryWithParams = GetLineQuery(text);
    var lines = PerformQuery(queryWithParams);
    
    return Ok(lines);
}

[HttpGet("PlayAndLine")]
public ActionResult PlayAndLine(string play, string text)
{
    string queryWithParams = GetPlayAndLineQuery(play, text);
    var lines = PerformQuery(queryWithParams);

    return Ok(lines);
}

All very straightforward so far, but now comes the “creative” approach to handling the Json problems.

I put my ElasticSearch queries into their own files. The first is Line.Json

{
    "query": {
        "match_phrase_prefix" :{
            "Line": ""
        }
    }
} 

And the second is PlayAndLine.Json

{
    "query":{
        "bool": {
            "must": [
                { "match": { "Play": "" } }
               ,{ "match_phrase_prefix": { "Line": "" } }
            ]
        }
    }
}

These Json queries are loaded into dynamic objects and the relevant values are set in C#.
See lines 5 and 14 & 15.

private string GetLineQuery(string text)
{
    string elasticSearchQuery = System.IO.File.ReadAllText($"Queries/Line.json");
    dynamic workableElasticSearchQuery = JsonConvert.DeserializeObject(elasticSearchQuery);
    workableElasticSearchQuery.query.match_phrase_prefix.Line = text;

    return workableElasticSearchQuery.ToString();
}

private string GetPlayAndLineQuery(string play, string text)
{
    string elasticSearchQuery = System.IO.File.ReadAllText($"Queries/PlayAndLine.json");
    dynamic workableElasticSearchQuery = JsonConvert.DeserializeObject(elasticSearchQuery);
    workableElasticSearchQuery.query.@bool.must[0].match.Play = play;
    workableElasticSearchQuery.query.@bool.must[1].match_phrase_prefix.Line = text;

    return workableElasticSearchQuery.ToString();
}

The strings the above methods return are the queries that will be sent to ElasticSearch.

The below method makes the request, and deserializes the response into the ESResponse class. That class was generated by https://json2csharp.com/.

private ESResponse PerformQuery(string queryWithParams)
{
    var response = _lowLevelClient.Search<StringResponse>("shakespeare", queryWithParams);
    ESResponse esResponse = System.Text.Json.JsonSerializer.Deserialize<ESResponse>(response.Body);
    return esResponse;
}

You might have noticed that I use System.Text.Json and Newtonsoft Json, this is because System.Text.Json does not support dynamic deserialization, see this discussion – https://github.com/dotnet/runtime/issues/29690.

That’s it, searching, and parsing of ElasticSearch results via a Web API application, feels a bit messy, but hope it helps.

Full source code available here.