CSharp Rest API

From bibbleWiki
Jump to navigation Jump to search

Introduction

This is a quick refresher for C# Rest API

Installation

I am now on SDK 8.0 and found the following issues

Use Apt not Snap

Using snap will mean your SDK cannot be found

sudo apt-get install dotnet-sdk-8.0

Create new Project

This is painless and can be done with

dotnet new webapi -n Catalog

Setting up Certificates

The next thing is run the project with F5. This warns you that you are not https. You fix this with the commands below but make sure you restart chrome

dotnet dev-certs https --clean
dotnet dev-certs https --check --trust

Dependency Injection DI

This just points out how to inject one service into another. With the example below we only use the Repository once so creating it as its own system is a bit silly. Anyway this is how to do it.

builder.Services.AddSingleton<IRepository>(provider =>
{
    BsonSerializer.RegisterSerializer(new GuidSerializer(GuidRepresentation.Standard));
    BsonSerializer.RegisterSerializer(new DateTimeOffsetSerializer(BsonType.String));

    var mongoDbSettings = builder.Configuration.GetSection(nameof(MongoDbSettings)).Get<MongoDbSettings>();
    var client = new MongoClient(mongoDbSettings.ConnectionString);
    return new MongoDBItemsRepository(client);
});

builder.Services.AddSingleton<IItemService>(provider => new ItemService(provider.GetRequiredService<IRepository>()));

Express C# Style

This is an example of using the minimal api from Microsoft. The import thing is the minimal API is a new approach to reduce the code required to get going. The AddSingleton is a way to use Dependency Injection. I found the errors unreadable from the framework but maybe used to working at a lower level. Quite easy to see how to get going

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<IItemService>(provider => new ItemService(new InMemItemsRepository()));
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

app.UseStatusCodePages(async statusCodeContext
    => await Results.Problem(statusCode: statusCodeContext.HttpContext.Response.StatusCode)
        .ExecuteAsync(statusCodeContext.HttpContext));

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.MapGet("/items", (IItemService itemService) =>
    TypedResults.Ok(itemService.GetItems()))
    .WithName("GetItems")
    .WithOpenApi();


app.MapGet("/items/{id}", Results<Ok<ItemDto>, NotFound> (IItemService itemService, Guid id) =>
        itemService.GetItem(id) is { } itemDto
            ? TypedResults.Ok(itemDto)
            : TypedResults.NotFound()
    )
    .WithName("GetItemById")
    .WithOpenApi();


app.Run();

C# Controllers Gotchas

In my examples we are using the minimal api approach. Previously for C# we used the Controller approach. The framework, by default relies on names of functions and when implemented with, for instance GetItemAsync this would cause a problem as it expects GetItem. To make the framework work you need to add the following to the ConfigServices. To me this seems a bit of a shoddy approach to a framework.

services.AddControllers(option => {
    options.SuppressAsyncSuffixInActionNames = false;
})

Secrets Manager

With dotnet comes the secrets manager for storing ice cubes :) To use it we do

dotnet user-secrets init
dotnet user-secrets set MongoDbSettings:Password NotSaying!!!

I really hate the hidden approach Microsoft was. We used MongoDbSettings because this is the section in appsettings.json. It magically reads the values from secrets manager. I would much prefer to have an indication of this somewhere.

Health Checks

Next Health checks. Heartbeating etc

// Add Service
builder.Services.AddHealthChecks();

//HealthCheck Middleware
app.MapHealthChecks("/healthz");

For MongoDb we have

dotnet add package AspNetCore.HealthChecks.MongoDb

So for me the mongoDB driver was incompatiable with the HealtChecks package so downgraded to 2.28.0.0 but once working needed to adjust the code for the minimal api. The convention is to have a live and a ready endpoint like kubernetes.

Define the Read Check

This is the healthcheck the package defines. Note the ready tag we have added.

builder.Services.AddHealthChecks()
   .AddMongoDb(mongoDbSettings.ConnectionString, name: "mongodb", timeout: TimeSpan.FromSeconds(3), tags: ["ready"]);

Simple live Define

We just return we are running and don't include any options

app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = (_) => false
});

Ready Define

The main point is we only look at the healthoptions which are tagged ready. The Response writer part is how to format the response. For me copilot did all of the work.

app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = (check) => check.Tags.Contains("ready"),
    ResponseWriter = async (context, report) =>
    {
        var result = JsonSerializer.Serialize(
            new
            {
                status = report.Status.ToString(),
                checks = report.Entries.Select(entry => new
                {
                    name = entry.Key,
                    status = entry.Value.Status.ToString(),
                    exception = entry.Value.Exception is not null ? entry.Value.Exception.Message : "none",
                    duration = entry.Value.Duration.ToString()
                })
            }
        );

        context.Response.ContentType = MediaTypeNames.Application.Json;
        await context.Response.WriteAsync(result);
    }
});

Other Health Checks

[Here] is a source of health checks you could use.

C# Extensions

I never used this a lot in the first 7 years of C#. For the last 3 it was all the rage. Basically they are functions which take this as the first argument

namespace Catalog.Extensions;

using Catalog.Dtos;
using Catalog.Entities;

public static class Extensions
{
    public static ItemDto AsDto(this Item item)
    {
        return new ItemDto
        {
            Id = item.Id,
            Name = item.Name,
            Price = item.Price,
            CreatedDate = item.CreatedDate
        };
    }
}

So now you can do this

    public IEnumerable<ItemDto> GetItems()
    {
        var itemDtos = repository.GetItems().Select(item => item.AsDto());

        return itemDtos;
    }

Dockerizing Our Stuff

This is not C# but I can never have enough examples of docker. Use it all day everyday and it just works so I never remember why. Here goes.

  • Create Dockerfile
  • Build Docker File
  • Create Docker Network
  • Restart MongoDB on network
  • Start App in Docker

Create Dockerfile

I have never done this, this way before. You can use the vs code docker extension, answer some easy questions and hey presto. You probably should review the output but mine was pretty pretty good.

Build Docker File

You suggested but I used

# docker build -t catalog:v1 . 
docker buildx build -t catalog:v1 .

Create Docker Network

docker network  create restapicsharp

Restart MongoDB on network

docker run -d --rm --name mongo -p 27017:27017 -v mongodata -e MONGO_INITDB_ROOT_USERNAME=mongoadmin -e  MONGO_INITDB_ROOT_PASSWORD=notsayagain!! --network restapicsharp   mongo

Start App in Docker

docker run  -it --rm -p 5193:5193 -e MongoDbSettings:Host=mongo -e MongoDbSettings:Password=nononono### --network restapicsharp   catalog:v2

Running in Kubernetes

Introduction

I had expected to be able to make two pods and run this in kubernetes and we would be away. But it was not to be and I quite enjoyed learning a tad more kubernetes and mongodb.

Catalog C# Stuff

This was straight forward. Just make a service and pod and good to go. I guess I did learn about local repositories with microk8s, secrets and how to override the appsettings.json with kubernetes using the double underscores (dunder) to represent object name->key.

Secrets

Not too had

kubectl create secret generic catalog-secrets --from-literal=mongodb-password='nonono###'

Provision Service and Pod

apiVersion: apps/v1
kind: Deployment
metadata:
  name: catalog
spec:
  selector:
    matchLabels:
      app: catalog
  template:
    metadata:
      labels:
        app: catalog
    spec:
      containers:
      - name: catalog
        image: localhost:32000/catalog:v12
        resources:
          limits:
            memory: "128Mi"
            cpu: "500m"
        ports:
        - containerPort: 5193
        env:
        - name: MongoDbSettings__Host
          value: mongodb-service
        - name: MongoDbSettings__User
          value: "mongoadmin"
        - name: MongoDbSettings__Password
          valueFrom:
            secretKeyRef:
              name: catalog-secrets
              key: mongodb-password

        livenessProbe:
          httpGet:
            path: /health/live
            port: 5193
        readinessProbe:
          httpGet:
            path: /health/ready
            port: 5193

---
apiVersion: v1
kind: Service
metadata:
  name: catalog-service
spec:
  type: LoadBalancer
  selector:
    app: catalog
  ports:
  - port: 5193
    targetPort: 5193

Mongodb

Introduction

Was a bit of a trial mainly because I have not done too much on the admin side for mongodb, only the usage of it. Add to that we are using stateful sets in kubernetes.

PV Persistent Volumnes

This was new to me too as I was all done for me previously. The PV are, in my case, provisioned on my machine using the microk8s.io/hostpath provider. We start off by making a storage class. This allows you to

  • Say when to make (bind) the request
  • Where to put the data

Pretty easy once you know.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: ssd-hostpath
provisioner: microk8s.io/hostpath
reclaimPolicy: Delete
parameters:
  pvDir: /opt/k8s/mongodb/data
volumeBindingMode: WaitForFirstConsumer

Now we have our class we are ready to deal with mongodb

Stateful Sets

This allows you, when using replicas, to ensure the resources are kept in sync. This is pretty much as you would expect after googling. I guess the storageClassName is pretty obvious. However

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mongodb-statefulset   
  namespace: catalog
spec:
  selector:
    matchLabels:
      app: mongo
  serviceName: "mongodb-service"
  replicas: 1
  template:
    metadata:
      labels:
        app: mongo
        selector: mongo
    spec:
      containers:
      - name: mongodb
        image: mongo
        ports:
        - containerPort: 27017
        env:
        - name: MONOGO_INITDB_ROOT_USERNAME
          value: "mongoadmin"
        - name: MONOGO_INITDB_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: catalog-secrets
              key: mongodb-password
        volumeMounts:
        - name: mongo-data
          mountPath: /data/db
  volumeClaimTemplates:
  - metadata:
      name: mongo-data
      namespace: catalog
    spec:
      storageClassName: ssd-hostpath
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi
---
apiVersion: v1
kind: Service
metadata:
  name: mongodb-service
  namespace: catalog
  labels:
    app: mongo
spec:
  clusterIP: None    
  selector:
    app: mongo
  ports:
  - protocol: TCP
    port: 27017
    targetPort: 27017

Problems

This is pretty much like the docker approach, a volume and all of the environment variables. One thing to note is the service is ClusterIP:None which means we can use the service name as the hostname in the connection string. The main issue is the MONOGO_INITDB_ROOT_USERNAME and MONOGO_INITDB_ROOT_PASSWORD which suggested we could to this

mongodb://mongoadmin:fred1y@mongodb-service:27017

And all would be well. It wasn't and reading up I thing the init script behaves differently in kubernetes. I don't know why is the answer. But I did learn some tricks. We can run a client in the network to test connecting with

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: mongo-client
  name: mongo-client
  namespace: catalog
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mongo-client
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: mongo-client
    spec:
      containers:
      - image: mongo
        name: mongo-client
        env:
        - name: mongo-client_INITDB_ROOT_USERNAME
          value: 'mongoadmin'
        - name: mongo-client_INITDB_ROOT_PASSWORD
          value: 'admin'

Then we can use

microk8s kubectl exec deployment/mongo-client -it -- /bin/bash

mongosh mongodb://myuser:mypass@mongodb-service:27017

To fix this you need to create the admin user yourself through the client. Connect with no user or password

mongosh mongodb://mongdb-service:27017

And run the following

db.createUser(
    {
        user: "mongoadmin",
        pwd: "mypass",
        roles: [ "root" ]
    }
)