Monday 8 January 2024

Docker repo

With our Azurite docker image running, it's time to configure our docker repo. Let's start by adding a docker profile to our 'launchSettings.json' file


"Docker": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5050",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},

As I've mention in the past, your naming conventions are very important, they are what let future you figure out what the hell you did; in this case all you're going to have to remember is that you have to check your 'launchSettings.json' file and you'll be able to follow the bread crumb trail of what this profile is meant for.

before we continue we're going to have to add a nuget to our project to work with azureBlobStorage, go to your .csproj file and enter in the following command 

dotnet add package azure.storage.blobs

you should see the following in your terminal

We need to add one more nuget package aht that is the 'azure.Identity' package

Your .csproj should now have the"azure.storage.blobs" package reference added

<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="azure.storage.blobs" Version="12.19.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>

</Project>

these will let us leverage Azures prebuilt classes for interacting with our blob storage.

Before we dive into our DockerRepo class, let's open our app settings file and add our azurite development connection string

{
"flatFileLocation": "DefaultEndpointsProtocol=https;
AccountName=devstoreaccount1;
AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;
BlobEndpoint=https://127.0.0.1:10000/devstoreaccount1;"
}

I broke the string up onto multiple lines for ease of reading, however you'll have to concatinate it onto one line.

Now we can finally start coding Next open up your 'DockerRepo.cs' file, it should look like the following


using pav.mapi.example.models;

namespace pav.mapi.example.repos
{
public class DockerRepo : IRepo
{
public Task<IPerson[]> GetPeopleAsync()
{
throw new NotImplementedException();
}

public Task<IPerson> GetPersonAsync(string id)
{
throw new NotImplementedException();
}
}
}

We configured the class but never implemented the methods, Let's take the opportunity to do so now; we're going to create a constructor that takes in a connection string and a the logic for our two get functions.

using System.Text.Json;
using Azure.Storage.Blobs;
using pav.mapi.example.models;

namespace pav.mapi.example.repos
{
public class DockerRepo : IRepo
{
BlobContainerClient _client;
public DockerRepo(string storageUrl)
{
this._client = new BlobContainerClient(storageUrl, "flatfiles");
}

public async Task<IPerson[]> GetPeopleAsync()
{
var openingsBlob = _client.GetBlobClient("people.json");
var openingJSON = (await openingsBlob.DownloadContentAsync()).Value.Content.ToString();

if (openingJSON != null)
{
var opt = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};

return JsonSerializer.Deserialize<Person[]>(openingJSON, opt);
}

throw new Exception();
}

public async Task<IPerson> GetPersonAsync(string id)
{
var ppl = await this.GetPeopleAsync();
if(ppl != null)
return ppl.First(p=> p.Id == id);
throw new KeyNotFoundException();
}
}
}

Now if we take a look at our main

using pav.mapi.example.models;
using pav.mapi.example.repos;

namespace pav.mapi.example
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
var flatFileLocation = builder.Configuration.GetValue<string>("flatFileLocation");

if (String.IsNullOrEmpty(flatFileLocation))
throw new Exception("Flat file location not specified");

if(builder.Environment.EnvironmentName != "Production")
switch (builder.Environment.EnvironmentName)
{
case "Local":
builder.Services.AddScoped<IRepo, LocalRepo>(x => new LocalRepo(flatFileLocation));
goto default;
case "Development":
builder.Services.AddScoped<IRepo, DockerRepo>(x => new DockerRepo(flatFileLocation));
goto default;
default:
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
break;
}

var app = builder.Build();

switch (app.Environment.EnvironmentName)
{
case "Local":
case "Development":
app.UseSwagger();
app.UseSwaggerUI();
break;
}

app.MapGet("/v1/dataSource", () => flatFileLocation);
app.MapGet("/v1/people", async (IRepo repo) => await GetPeopleAsync(repo));
app.MapGet("/v1/person/{personId}", async (IRepo repo, string personId) => await GetPersonAsync(repo, personId));

app.Run();
}

public static async Task<IPerson[]> GetPeopleAsync(IRepo repo)
{
return await repo.GetPeopleAsync();
}

public static async Task<IPerson> GetPersonAsync(IRepo repo, string personId)
{
var people = await GetPeopleAsync(repo);
return people.First(p => p.Id == personId);
}
}
}

we pass our connection string to our Docker repo service in the form our flatfilelocation variable which is populated based on the profile loaded.

Now if we run our application with the 'local' profile our static GetPersonAsync and GetPeopleAsync functions will receive the LocalRepo implementation of our IRepo interface and if we run our application with the 'docker' profile, it will use the DockerRepo implementation of our IRepo interface.