firstly I want to give a shoutout to a guy named Les Jackson. He put a video up on youtube that is an 11 hour course on microservices. If you have not watched, you can find it here
I really wanted to drive some of these concepts home for myself and possible others in the future, so I documented my entire 11 hour journey into this repo. Again, this not my work, but an inspiration, and something that can refer back to in the future when implementing microservices.
firstly we are going to create a blank folder in our dev directory
then we are going to initialize it
npm init -y
then let's create a README.md in the root of the directory for our notes.
now we are going to create a file in the root called .gitignore. We'll add stuff to it as we see the need. This is not going to be a typical node project, so we won't use npx gitignore node.
now initial the git repository
git init
git add .
git commit -m "initial commit"
then we are going to go ahead and link this up with our github repo. Refresh the page and we should see our readme and our repo should be setup and we are ready to get started.
firstly, we will create our first branch to work off of
git checkout -b branch1
Here we are going to take a look at slides 01 through 03. I am not a powerpoint guy, but I give my best shot:
now we should play through the service architechture powerpoint presention:
now we should play through the platform service Architecture presention:
now we should play through the command service Architecture presentation:
thats it for our introduction into what we are going to build, so let's commit this sad powerpoint work and get to writing some code.
let's get started by creating a few projects
first lets check out version of .NET to make sure everything is cozy, so let's run
dotnet --version
for our first project, we are going to run this command
dotnet new webapi -n PlatformService
now our folder structure should look something like this:
we can open that folder in vscode by typing
code -r PlatformService
now when this opens, you may see a prompt like this:
definately choose yes, becuase this will give you the ability to debug your applications with ease.
now your folder structure inside of platform service should look like this:
the first thing we are going to do with this project is to delete the WeatherForecast.csl file and delete the Controller for that as well.
let's open the PlatformService.csproj file so we can make sure that as we install our dependencies, that they actually get installed. This will be helpfull fo sure. So, let's install some dependencies:
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
now your PlatformService.csproj should look like this:
just for clarity, i'll put the code here are well
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.10">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.10" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
</ItemGroup>
</Project>
now, create a folder in the root of our project called Models and create a file inside of there called Platform.cs. This is what this file should look like. Its pretty straight forward
as a side note, once your are in a class, you can add properties with the shortcut by just typing prop, and then you will get intellisence which will help you out.
namespace PlatformService.Models
{
public class Platform
{
public int Id { get; set; }
public string? Name { get; set; }
public string? Publisher { get; set; }
public string? Cost { get; set; }
}
}
now we are going to add some annotations to this, but we are going to get some squigglies, so let's take a look at how to fix those squigglies:
so, to fix this, just put the cursor inside of the offending word, and press ctrl-. and you will get some intellisense that will allow you to import the correct using statement that you need.
so, now our class should look like this:
using System.ComponentModel.DataAnnotations;
namespace PlatformService.Models
{
public class Platform
{
[Key]
[Required]
public int Id { get; set; }
[Required]
public string? Name { get; set; }
[Required]
public string? Publisher { get; set; }
[Required]
public string? Cost { get; set; }
}
}
now, we are going to create our DbContext. Create a folder in the root of the project called Data and add a file inside of that called AppDbContext.cs
another shortcut that you can use is for creating constructors, so you just basically just type ctor and vscode will help you out. just press tab and vscode will automatically create a constructor for you.
using Microsoft.EntityFrameworkCore;
using PlatformService.Models;
namespace PlatformService.Data
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> opt) : base(opt)
{
}
public DbSet<Platform> Platforms { get; set; }
}
}
then we need to wire this up on our Program.cs file
builder.Services.AddDbContext<AppDbContext>(opt => opt.UseInMemoryDatabase("InMem"));
now we are going to add our repository. let's create an interface in the data folder. make a file named IPlatformRepo.cs
using PlatformService.Models;
namespace PlatformService.Data
{
public interface IPlatformRepo
{
bool SaveChanges();
IEnumerable<Platform> GetAllPlatforms();
Platform GetPlatformById(int id);
void CreatePlatform(Platform plat);
}
}
now create the concrete class to inherit from out interface. Create a filed called PlatformRepo.cs in the data folder
one thing you will notice right off, is that vscode is not happy with us:
we can use the same technique by putting the cursor inside of the word IPlatformRepo and pressing ctrl-. to get some intellisense:
after you click on implement interface, vscode will automatically stub out all of the methods that you need in order to fulfill the contract between the two.
it should not look like this:
using PlatformService.Models;
namespace PlatformService.Data
{
public class PlatformRepo : IPlatformRepo
{
public void CreatePlatform(Platform plat)
{
throw new NotImplementedException();
}
public IEnumerable<Platform> GetAllPlatforms()
{
throw new NotImplementedException();
}
public Platform GetPlatformById(int id)
{
throw new NotImplementedException();
}
public bool SaveChanges()
{
throw new NotImplementedException();
}
}
}
let's start to fill this file out. we should start with a constructor first. don't forget about the ctor shortcut
let's look at a stub for this what we will use a lot in the upcoming exercises
to fix this, put the cursor in the _context word and press ctrl-. and select create private read-only field:
let's start at the bottom and fill out this class
public bool SaveChanges()
{
return (_context.SaveChanges() >= 0);
}
public IEnumerable<Platform> GetAllPlatforms()
{
return _context.Platforms.ToList();
}
public Platform GetPlatformById(int id)
{
var platform = _context.Platforms.FirstOrDefault(p => p.Id == id);
if (platform == null)
{
return new Platform { };
}
else
{
return platform;
}
}
public void CreatePlatform(Platform plat)
{
if (plat == null)
{
throw new ArgumentNullException(nameof(plat));
}
_context.Platforms.Add(plat);
}
now our final PlatformRepo.cs should look like this:
using PlatformService.Models;
namespace PlatformService.Data
{
public class PlatformRepo : IPlatformRepo
{
private readonly AppDbContext _context;
public PlatformRepo(AppDbContext context)
{
_context = context;
}
public void CreatePlatform(Platform plat)
{
if (plat == null)
{
throw new ArgumentNullException(nameof(plat));
}
_context.Platforms.Add(plat);
}
public IEnumerable<Platform> GetAllPlatforms()
{
return _context.Platforms.ToList();
}
public Platform GetPlatformById(int id)
{
var platform = _context.Platforms.FirstOrDefault(p => p.Id == id);
if (platform == null)
{
return new Platform { };
}
else
{
return platform;
}
}
public bool SaveChanges()
{
return (_context.SaveChanges() >= 0);
}
}
}
inn order to be able to inject this into our constructors, we need to register this with our Program.cs file. This is an important step in the process:
so, in Program.cs, add this little snippet of code:
builder.Services.AddScoped<IPlatformRepo, PlatformRepo>();
now, just to keep things tidy, we are going to make sure that our project builds:
dotnet build
now, you may see a warning here, but we'll take care of that later on:
before we commit this, we need to add some stuff to our gitignore, because right now there appears to be a lot of files to commit. first let's check to see where they are coming from:
add this to the .gitignore file
bin
obj
ok, now we can commit this branch before we move on
now we are going to seed out in-memory database so create a file in the Data folder called PrepDb.cs
let's just stub out the start of this file so we can add it to our Program.cs file:
namespace PlatformService.Data
{
public static class PrepDb
{
public static void PrepPopulation(IApplicationBuilder app, bool isProd)
{
}
}
}
then in our Program.cs file, let's add this snippet just before the app.Run() command:
PrepDb.PrepPopulation(app, app.Environment.IsProduction());
At first, our file looks like this:
so, lets get rid of that warning. go into the PlatformService.csproj file and commend out this line
now you wll see that this error will go away
new this file should look like this:
using PlatformService.Models;
namespace PlatformService.Data
{
public static class PrepDb
{
public static void PrepPopulation(IApplicationBuilder app, bool isProd)
{
using (var serviceScope = app.ApplicationServices.CreateScope())
{
SeedData(serviceScope.ServiceProvider.GetService<AppDbContext>(), isProd);
}
}
private static void SeedData(AppDbContext context, bool isProd)
{
if (!context.Platforms.Any())
{
Console.WriteLine("--> Seeding Data...");
context.Platforms.AddRange(
new Platform() { Name = "Dot Net", Publisher = "Microsoft", Cost = "Free" },
new Platform() { Name = "SQL Server Express", Publisher = "Microsoft", Cost = "Free" },
new Platform() { Name = "Kubernetes", Publisher = "Cloud Native Computing Foundation", Cost = "Free" }
);
context.SaveChanges();
}
else
{
Console.WriteLine("--> we already have data");
}
}
}
}
now just for safety, lets run a build
dotnet build
everything should be good. oh wait, i noticed we got a lot of nullable warnings so lets go back to our Platform.cs class in the models folder and remove the ? character
now let's run this command
dotnet build
and we should see this:
we want to make sure that we are seeing the Seeding Data in our console.
now we are going to create some DTOs {Data Transformation Object}
so, lets create another folder in the root of our project called Dtos and create a file inside of that called PlatformReadDto.cs
namespace PlatformService.Dtos
{
public class PlatformReadDto
{
public int Id { get; set; }
public string Name { get; set; }
public string Publisher { get; set; }
public string Cost { get; set; }
}
}
now let's create another Dto called PlatformCreateDto.cs
using System.ComponentModel.DataAnnotations;
namespace PlatformService.Dtos
{
public class PlatformCreateDto
{
[Required]
public string Name { get; set; }
[Required]
public string Publisher { get; set; }
[Required]
public string Cost { get; set; }
}
}
so now we have our Dtos, but we need a way to map these to our model. let's commit what we have and in the next branch, we will create our mappers.
firstly, we need to register AutoMapper so in the Progam.cs file, we need to add this snippet:
builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
in the root of our project, we are going to create a new folder called Profiles and in there we are going to create a new file called PlatformProfiles.cs
using AutoMapper;
using PlatformService.Dtos;
using PlatformService.Models;
namespace PlatformService.Profiles
{
public class PlatformProfile : Profile
{
public PlatformProfile()
{
// source -> target
CreateMap<Platform, PlatformReadDto>();
CreateMap<PlatformCreateDto, Platform>();
}
}
}
now let's make sure we don't have any errors, so we'll do a run
dotnet run
now we are going to create our controller for this app, so let's create a file called PlatformsController.cs in the Controllers folder.
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using PlatformService.Data;
using PlatformService.Dtos;
namespace PlatformService.Controllers
{
[Route("api/Platforms")]
[ApiController]
public class PlatformsController : ControllerBase
{
private readonly IMapper _mapper;
private readonly IPlatformRepo _repo;
public PlatformsController(IPlatformRepo repo, IMapper mapper)
{
_repo = repo;
_mapper = mapper;
}
public ActionResult<IEnumerable<PlatformReadDto>> GetPlatforms()
{
Console.WriteLine("--> Getting Platforms");
var platformItem = _repo.GetAllPlatforms();
return Ok(_mapper.Map<IEnumerable<PlatformReadDto>>(platformItem));
}
}
}
now let's test everythig out up to this point, so we'll run ths command
dotnet run
now we can actually test our work finally. We are going to use insomnia to test our api. here is the link to the app or you can just type insomnia app into your brower and find the download
once that is loaded, we are going to go to the dashboard and create a new project:
now we have a clean slate to work with for this project
let create a new folder for this project:
and we are going to call this new folder PlatformService
now we are going to create a new request to test out our service:
now we need to figure out where our request needs to come from:
so, let's just fix this up by changing our ports, so open up Properties/luanchSettings.json and change the port to look like this:
"applicationUrl": "https://localhost:5001;http://localhost:5000",
now let's re-run the application
dotnet run
and we should see the ports change
now we are going to rename that request to 'Get all Platforms' and we are going to make it look like this:
we have our first successfull endpoint. let's move on shall we.
now let's add an endpoint for getting a platform by it's id
[HttpGet("{id}",Name = "GetPlatformById")]
public ActionResult<PlatformReadDto> GetPlatformById(int id)
{
var platformItem = _repo.GetPlatformById(id);
if (platformItem != null)
{
return Ok(_mapper.Map<PlatformReadDto>(platformItem));
}
else
{
return NotFound();
}
}
let's spin it up and give that a try
dotnet run
now in insomnia, let's test our request like this:
everything is working well, so we need one more endpoint, then on to docker and then on to kubernetes. so, let's add that final endpoint to create a new platform
[HttpPost]
public ActionResult<PlatformReadDto> CreatePlatform(PlatformCreateDto platformCreateDto)
{
var platformModel = _mapper.Map<Platform>(platformCreateDto);
_repo.CreatePlatform(platformModel);
_repo.SaveChanges();
var platformReadDto = _mapper.Map<PlatformReadDto>(platformModel);
return CreatedAtRoute(nameof(GetPlatformById), new { Id = platformReadDto.Id }, platformReadDto);
}
now let's test it out
dotnet run
and in insomnia, let's create a test like this: one note, for this request, because it's going to be a post, we need to tell insomnia that we have a json body
we can also look at the headers to see the location parameter
now if we run our Get all Platforms again, we should see our new platform
before we wrap up this branch, let's take a look at debugging our app, as that is very important when things don't go as planned. To do this, let's look at the debug section of vscode.
hit the play button and run that endpoint in insomnia, and you should see the code break:
we can also see that our app is running on two different ports:
let's take a look at our swagger documentation
make sure you have docker desktop running firstly:
now make sure to install the plugin into vscode
in the root of our project create a file called Dockerfile
here is how i am finding the image:
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-env
WORKDIR /app
COPY *.csproj ./
RUN dotnet restore
COPY . ./
RUN dotnet publish -c Release -o out
FROM mcr.microsoft.com/dotnet/aspnet:6.0
WORKDIR /app
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "PlatformService.dll"]
make sure the filename matches with whats in the bin directory
now let's check our version of docker
let's now open up the Docker Desktop app and check to make sure that we have kubernetes enabled:
now let's build our first docker file
docker build -t c5m7b4/platformservice .
be sure to use your docker hub username instead of mine and don't forget the period at the end, because that is the directory that it need to look to to find the Dockerfile. now your bash is going to go crazy, so no worries there and after it finishes doing all its work, you should be able to see your new image:
and you should also be able to see it in the Docker Desktop app:
let's spin up our image and give it a test run
docker run -p 8080:80 -d c5m7b4/platformservice
firstly, you should get a hash back letting us know our image is running, and you should also be able to see that the container is running in the Docker plugin in vscode. there are a few more things that we can do also
docker ps
this will show us all running containers
if you want to see all containers that you might have that are not running, you can try this command
docker ps -a
if you want to see all images that you've created
docker images
you can also stop the container by using it's id
docker stop
sometimes it takes a little time to stop the container
one thing to note is that if we run the container again using the same command as before:
docker run -p 8080:80 -d c5m7b4/platformservice
notice that now, we have two of these containers.
so let's run docker ps again and get the container id so we can stop it
docker stop 2224fc5941c3
so, to start a stopped container, use the id that was created for your
docker start 2224fc5941c3
you can also use the plugin
now we are going to push our image up to docker hub. I'm not sure, but I think you have to do a docker login first, and then after that it should remember your credentials:
docker push c5m7b4/platformservice
this will take a few minutes, but after it's finished, we can log into dockerhub and see our new image which we will need for kubernetes
next up, we'll test out our container
let's find out what containers that we have that we can start up first
docker ps -a
now let's start our container up so we can test it
docker start 6dcbea6544d9
you will see the container running in the Docker Desktop
lets now create a new folder in insomnia
im going to call it Docker env, then create another folder inside of that for our PlatformService. Inside of that, create a new request called Get all Platforms
make sure you use port 8080 as that is what we specified when we spun up our container.
that should be good enough for now. YOu can create and test out the other two endpoints, but the point of all of this is to get to kubernetes. this was just a means to an end. so lets stop here and finally get on to kubernetes
kubernetes is a container orchestrator. it handles automatically restarting container even if they stop. it's often referred as k82 because of the number of letters
there are two main types of users
- developers
- administrators
we are to have basicaly containers and under that we will have pods
here we are going to look at the kubernetes powerpoint
let's make sure our folder structure looks something like this. You probably won't have images or powerpoints, but you should at least have the PlatformService folder
we are going to create a new folder called K8S. this folder is just going to hold our kubernetes deploy files, so let's open that up in vscode
we are going to do the first part of the powerpoint presention, so create a file called platforms-depl.yaml
it also might help with this to install this plugin into vscode
apiVersion: apps/v1
kind: Deployment
metadata:
name: platforms-depl
spec:
replicas: 1
selector:
matchLabels:
app: platformservice
template:
metadata:
labels:
app: platformservice
spec:
containers:
- name: platformservice
image: c5m7b4/platformservice:latest
this will basically deploy a container into a pod. spaced here are critical so we might run into some problems here, but we'll keep our fingers crossed as we move forward and test things out.
now we are going to try to run our deployment file
first, let's make sure that kubernetes is ready so run this command
kubectl version --short
and you should see something like this:
kubectl will be the command that we will use for everything kubernetes related as we will see in the near future.
to deploy our container, we are going to run this command:
kubectl apply -f platforms-depl.yaml
hopefully you will see this:
now let's check to see if this really worked or not
kubectl get deployments
at this point, depending on your computers specs, you may see that the ready column might have 0/1, so it might take a bit of time to spin all this up.
let's take a look at our pods
kubectl get pods
also at this time, you might need to run this a few times, while waiting for all this to complete
if you take a look at our docker plugin, you will see that we are up and running:
if we now go to our docker desktop, it should look like this:
you can also click on the platformservice and see what the logs look like:
notice our seeding data console log that we provided
congrats, you just deployed your first kubernetes project
you may notice some weirdness when you commit your changes and then checkout the master branch, but it will sort itself out.
if it makes any difference, we can destroy and redeploy this like so:
kubectl delete deployment platforms-depl
and now docker desktop is empty
kubectl get deployments
lets now fire things back up
kubectl apply -f platforms-depl.yaml
now things should look good in the vscode plugin and in Docker Desktop
now we are going to create a node port so we can access this container from our local computer
let's create a file in our K8S project called platforms-np-srv.yaml the np is for node port
and it should look like this:
apiVersion: v1
kind: Service
metadata:
name: platformnpservice-srv
spec:
type: NodePort
selector:
app: platformservice
ports:
- name: platformservice
protocol: TCP
port: 8070
targetPort: 80
i am going to experiment here a little bit and deviate from the tutorial, and I am going to try to use port 8070, because I normally have IIS running on my computer and it takes port 80, which was specified in the tutorial, but I want to test this out on port 8070. We'll see how that goes for me 🙈 I'm pretty sure the port 80 if the port of the service and the port 8070 is the port for my computer, but I guess we'll find out when we test it out. easy fix, if that's not the case though.
kubectl apply -f platforms-np-srv.yaml
lets make sure it is working
kubectl get services
let's get brave and test everything out now. so go back to insomnia, and we are going to create a new folder for our K8S and inside of that, create a new folder called PlatformService. then we are going to create a new test called Get all Platforms
!!!!!!!!!!!!!!!!!!!!! disclaimer, I made a mistake. I though that the port 8070 was the one that i need to use but after some testing, i realized that this is the internal port, so we are going to delete this deployment, fix our mistake and then make it right.
so in our platforms-np-srv.yaml file, make these changes
apiVersion: v1
kind: Service
metadata:
name: platformnpservice-srv
spec:
type: NodePort
selector:
app: platformservice
ports:
- name: platformservice
protocol: TCP
port: 80
targetPort: 80
then let's remove our deployment
kubectl delete service platformnpservice-srv
now our service should be gone, so let's recreate it the correct way
kubectl apply -f platforms-np-srv.yaml
now let see what our services look like
kubectl get services
so now the ip that we need to test with is going to be the 31637 port, so let's setup insomnia like this:
all right!!!! this all looks really good. congrats if you made it this far. the next steps is to create our command service and get these boys talking to each other synchronously and asynchronously
let go to a command line at the root of our project and run this command
dotnet new webapi -n CommandsService
now our folder stucture should look like so:
you will probably see this box pop up, so hit yes, so we can properyly debug this application
now let's open that in vscode and get started. open the CommandsService.csproj file so we can make sure that our dependencies that we are about to install do in fact show up.
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.InMemory
so now our CommandsService.csproj file should look like this:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.10">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.10" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
</ItemGroup>
</Project>
now, let's cleanup our project by removing the references to the weatherforcast
now in the properties folder under launchSetting.json, let's change our ports so when we fire up both services, the ports dont conflict with each other:
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:34219",
"sslPort": 44322
}
},
"profiles": {
"CommandsService": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:6001;http://localhost:6000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
now let's just make sure this will run
dotnet run
just make sure that everything is working ok.
now let's create our first controller, so in the controllers folder create a file called PlatformsController.cs
using Microsoft.AspNetCore.Mvc;
namespace CommandsService.Controllers
{
[Route("api/c/platforms")]
[ApiController]
public class PlatformsController : ControllerBase
{
public PlatformsController()
{
}
[HttpPost]
public ActionResult TestInboundConnection()
{
Console.WriteLine("--> Inbound Post # command service");
return Ok("Inbound test ok from platforms controller");
}
}
}
now we are going to test this with insomnia:
let's do a dotnet run command
dotnet run
it should be running on port 6000
let'g go to insomnia and create folder for testing this out by creating a folder for the Command Service. Let's create a new request called 'Test inbound connection'
if we look at our console in vscode, we should see that we got that
let's go back to the PlatformService application in vscode and we'll make an inefficient way of comunicating between services.
so, to test this out, we are going to create a new folder called SyncDataServices
inside of that folder, we are going to create another folder called Http
inside of here we are going to create an interface, so create a file called ICommandDataClient.cs
using PlatformService.Dtos;
namespace PlatformService.SyncDataServices.Http
{
public interface ICommandDataClient
{
Task SendPlatformToCommand(PlatformReadDto plat);
}
}
now we will create the concrete class to go with our interface so create a filed called CommandDataClient.cs in that same Http folder.
using System.Text;
using System.Text.Json;
using PlatformService.Dtos;
namespace PlatformService.SyncDataServices.Http
{
public class CommandDataClient : ICommandDataClient
{
private readonly HttpClient _httpClient;
private readonly IConfiguration _config;
public CommandDataClient(HttpClient httpClient, IConfiguration config)
{
_httpClient = httpClient;
_config = config;
}
public async Task SendPlatformToCommand(PlatformReadDto plat)
{
var httpContent = new StringContent(
JsonSerializer.Serialize(plat),
Encoding.UTF8,
"application/json");
var response = await _httpClient.PostAsync("http://localhost:6000/api/c/platforms", httpContent);
if (response.IsSuccessStatusCode)
{
Console.WriteLine("--> Sync POST to CommandSerice was OK!");
}
else
{
Console.WriteLine("--> Sync POST to CommandService was NOT OK!");
}
}
}
}
now we really dont like having that url hard-coded, so let's store that url in our config file. so go to the appsettings.Development.json file and add this into it:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"CommandService": "http://localhost:6000/api/c/platforms"
}
then let's replace the url call with our config variable
var response = await _httpClient.PostAsync(_config["CommandService"], httpContent);
now to use this, we need to register this in our Program.cs file like so, and we can put this just below the repo stuff.
builder.Services.AddHttpClient<ICommandDataClient, CommandDataClient>();
let's commit this, and in the next section, we will actually test all this out.
first let's inject our new service into the controller
public PlatformsController(IPlatformRepo repo, IMapper mapper, ICommandDataClient commandDataClient)
{
_repo = repo;
_mapper = mapper;
_commandDataClient = commandDataClient;
}
don't forget to add the create readonly field as well.
then come down to the post and make it look like this
[HttpPost]
public async Task<ActionResult<PlatformReadDto>> CreatePlatform(PlatformCreateDto platformCreateDto)
{
var platformModel = _mapper.Map<Platform>(platformCreateDto);
_repo.CreatePlatform(platformModel);
_repo.SaveChanges();
var platformReadDto = _mapper.Map<PlatformReadDto>(platformModel);
try
{
await _commandDataClient.SendPlatformToCommand(platformReadDto);
}
catch (System.Exception ex)
{
Console.WriteLine($"--> Could not send synchronously: {ex.Message}");
}
return CreatedAtRoute(nameof(GetPlatformById), new { Id = platformReadDto.Id }, platformReadDto);
}
now let's check things out, so we do a dotnet build and then a dotnet run
now let's open our CommandService application in another instance of vscode and do a dotnet run on it so they are both running. Now if we create a plaform in our PlatformService, it should alert the CommandService of the new platform.
now that we have both of those up and running, let's go back to insomnia and create a platform and see what's happening here.
so, if we look at the console for our platform service, we should see this:
and if we look at the console of our CommandService, we should see this:
one thing to note here that i did notice, is that if you are having problems with the two talking, especially using https, this command might be helpful to run on both projects
dotnet dev-certs https --trust
now if we kill the comand service and try to use the same endpoint, you will see that it's taking time.
it stil works, but here you can see that even though we specified asyn await, we still had to wait, and in our platformservice, we got this message:
let's get our command service running in kubernetes and setup our clusterip's for both:
before we can get our command service running in kubernetes, we need to dockerize it and push the image up to docker hub.
lets make sure that we are in our CommandService project and create a file in the root called Dockerfile:
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-env
WORKDIR /app
COPY *.csproj ./
RUN dotnet restore
COPY . ./
RUN dotnet publish -c Release -o out
FROM mcr.microsoft.com/dotnet/aspnet:6.0
WORKDIR /app
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "CommandsService.dll"]
then let's open a terminal and type this command:
docker build -t c5m7b4/commandservice .
then we'll push it up to docker hub
docker push c5m7b4/commandservice
we probably should have tested this before we pushed it, but we'll just do that now
docker run -p 8080:80 c5m7b4/commandservice
we should see this:
let's go ahead and stop that service
now it's time to go back to our K8s project
let's add this to the end of our platforms-depl.yaml file
---
apiVersion: v1
kind: Service
metadata:
name: platforms-clusterip-srv
spec:
type: ClusterIP
selector:
app: platformservice
ports:
- name: platformservice
protocol: TCP
port: 80
targetPort: 80
we'll leave that be for the moment and we will create a new deployment for our commandsService, so create a file called commands-depl.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: commands-depl
spec:
replicas: 1
selector:
matchLabels:
app: commandservice
template:
metadata:
labels:
app: commandservice
spec:
containers:
- name: commandservice
image: c5m7b4/commandservice:latest
---
apiVersion: v1
kind: Service
metadata:
name: commands-clusterip-srv
spec:
type: ClusterIP
selector:
app: commandservice
ports:
- name: commandservice
protocol: TCP
port: 80
targetPort: 80
it's basically just copying the platforms-depl.yaml file and change the names over. pretty basic setup.
we need to update our platformservice so open that project up
we need to let our production build know how to talk to the CommandService
so in appsettings.json, add this entry:
"CommandService": "http://commands-clusterip-srv:80/api/c/platforms"
now, since we have changes our image we need to rebuild and publish it again
docker build -t c5m7b4/platformservice .
docker push c5m7b4/platformservice
now we should be good to go
let check to see where we are at, so open up the K8s project
kubectl get deployments
we only have one deployment so far
kubectl get pods
kubectl get services
kubectl apply -f platforms-depl.yaml
now lets check our services
kubectl get services
everything should match here
we still need to force kubernetes to pull down the latest image, because although we created the service, it did not detect a change in our actual application
kubectl rollout restart deployment platforms-depl
now we should be set with that, so let's deploy our command service
kubectl apply -f commands-depl.yaml
now if we check our services
kubectl get services
now let check out deployments
kubectl get deployments
and now let's check our pods
kubectl get pods
all is looking pretty good so far.
now if we go and look at our docker desktop
we can probably kill the two dead ones while we are at it
but not it's time to actually test these puppies out.
let's test all this out with insomnia
all this is going to happen in our K8s folder that we created in insomnia, so let's just make sure that the Get all Platforms is still working
that stil looks good
looks good from the outside, but let's check the logs in docker desktop
notice that last line --> Ibound Post # command service
that's what we want to see. now in our platform service:
once again, notice the last line. That is what we want to see. If for some reason, we are getting some crazy error about certificates, we can use this line on both projects:
dotnet dev-certs https --trust
now, just to double check, run the Get all Platforms again and we should see our new platform:
!!!!!!!!!!!!!!!!! this is definately a start
as a side note, you may notice the redirection errors. that's becuase we are not using https. this error is coming from this line of code in both our platform and commands services:
if you look in the Program.cs file, you will find this line
app.UseHttpsRedirection();
we could comment this out, but then we would have to rebuild and re-push to docker hub, so we'll leave in the warnings for now.
if you've made it this far, congrats, but there is a ton of more cool shit that we need to do
this is where we have come so far
and this is the next step:
we are going to setup the ingress nginx container as our gateway. the node port is good for testing, but not good for production.
let's do a google search for ingress nginx
and we want to find the one from kubernetes
here is the link
then lets checkout the getting started section, and then the one about using docker desktop. we will probably have to explore a way for aws as well.
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.4.0/deploy/static/provider/aws/deploy.yaml
just for fun, type the url of this puppy into chrome and take a look at their yaml file. it's quite impressive.
but i digress...
let's but that whole thing in our clipboard, open up the K8S app because that is where we have been working with kubernetes. you don't actually have to do that, but I am trying to stay consistent
im going to paste that into my terminal and let it do its thing. Its actually pretty fast. now if we look at our docker plugin:
and if we look at docker desktop, we are going to see a ton of new stuff
now if we run
kubectl get deployments
notice that we dont see nginx.same for pods. nginx is running in its own namespace, so let's take a look at what we have there
kubectl get namespace
so, what we can do it this:
kubectl get pods --namespace=ingress-nginx
notice how 2 of them completed. nginx goes through an initialize phase where it will create pods to create other pods so now if we look at docker desktop again
so, we can probably just get rid of the ones that have exited
now if we look at the services:
kubectl get services --namespace=ingress-nginx
notice we have a load balancer by default.
make sure that you are in the K8S project and create a new file called ingress-srv.
this is going to be our routing file fron nginx to our services
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-srv
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/user-regex: 'true'
spec:
rules:
- host: acme.com
http:
paths:
- path: /api/platforms
pathType: Prefix
backend:
service:
name: platforms-clusterip-srv
port:
number: 80
- path: /api/c/platforms
pathType: Prefix
backend:
service:
name: commands-clusterip-srv
port:
number: 80
spacing is very critical here!!!!!
since we used acme.com, we need to change our host file to that acme.com is associated with something
my location is
C:\Windows\System32\drivers\etc
im going to add this line
127.0.0.1 acme.com
also, if you are running IIS, then make sure to stop if for this
now back in our K8S project, run this command
kubectl apply -f ingress-srv.yaml
and we should see this:
now it's actually time to test the beast
back to insomnia
Im going to rename our PlatformService under the K8s to PlatformServer (Node Port)
then create another folder called PlatformService (Nginx)
let now create a new test:
this time we are going to use acme.com
sweet, things are starting to come together. in the next section, we'll setup our sql server and then start to move on to rabbitmq for our message bus
now we are going to setup our first sql server, and this is going to need some storage, so let's look to see what storeageclasses we already have
kubectl get storageclass
there are three types of classes
- persistent volume claim
- persistent volume
- storeage class
back in our K8S project, let's create a new file called local-pvc.yaml for local persistent volume
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mssql-claim
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 200Mi
this is our first basic setup and it will allow for persistent storage requesting 200 MB of storage.
now we type this command:
kubectl apply -f local-pvc.yaml
as a note, if you have already done this, you can remove it and start from scratch by running this command:
kubectl delete pvc mssql-claim
now we can see what we have by running this command
kubeclt get pvc
now if we run
kubectl get pvc
we shoudl see this:
ok, for the next part, we are going to need to supply sql with an administrator password, so let's do some footwork first. let's check that we have not already stored a secret
kubeclt get secrtes
hopefully you are seeing this
if for some reason, you are doing this series again for practice, you might see this:
you can remove this by using this command:
kubectl delete secrets mssql
just trying to cover all the bases here. so now, we should be ready to get started. sorry for the long winded explanation.
now let's store our sa password into a secret. Now, obviously, this is a terrible thing altogether, but we'll probably come back aroung to this:
kubectl create secret generic mssql --from-literal=MSSQSL_SA_PASSWORD="pa55w0rd!"
now, i think we are ready to create our sql server so let's stop here for now.
now, we are back in our K8S project, we are going to create a new file called mssql-plat-depl.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: mssql-depl
spec:
replicas: 1
selector:
matchLabels:
app: mssql
template:
metadata:
labels:
app: mssql
spec:
containers:
- name: mssql
image: mcr.microsoft.com/mssql/server:2017-latest
ports:
- containerPort: 1433
env:
- name: MSSQL_PID
value: "Express"
- name: ACCEPT_EULA
value: "Y"
- name: MSSQL_SA_PASSWORD
valueFrom:
secretKeyRef:
name: mssql
key: MSSQL_SA_PASSWORD
volumeMounts:
- mountPath: /var/opt/mssql/data
name: mssqldb
volumes:
- name: mssqldb
persistentVolumeClaim:
claimName: mssql-claim
this is our basic setup, but we also have to add the networking, so we can copy the settings from our commands-depl.yaml file. we will copy the cluster section and make some neccessary changes:
---
apiVersion: v1
kind: Service
metadata:
name: mssql-clusterip-srv
spec:
type: ClusterIP
selector:
app: mssql
ports:
- name: mssql
protocol: TCP
port: 1433
targetPort: 1433
i really want to play with these ports because I have sql server running on my laptop, and i would like to find a way to have these not interfere with each other, so we may need to play with this a little bit, but let's just get all this up and running and then we can look into making this our bitch.
now we want to be able to connect to this from our local laptop, so we'll add a load balancer
---
apiVersion: v1
kind: Service
metadata:
name: mssql-loadbalancer
spec:
type: LoadBalancer
selector:
app: mssql
ports:
- protocol: TCP
port: 1433
targetPort: 1433
so our whole file should look like this:
apiVersion: apps/v1
type: Deployment
metadata:
name: mssql-depl
spec:
replicas: 1
selector:
matchLabels:
app: mssql
template:
metadata:
labels:
app: mssql
spec:
containers:
- name: mssql
image: mcr.microsoft.com/mssql/server:2017-latest
ports:
- containerPort: 1433
env:
- name: MSSQL_PID
value: "Express"
- name: ACCEPT_EULA
value: "Y"
- name: MSSQL_SA_PASSWORD
valueFrom:
secretKeyRef:
name: mssql
key: MSSQL_SA_PASSWORD
volumeMounts:
- mountPath: /var/opt/mssql/data
name: mssqldb
volumes:
- name: mssqldb
persistentVolumeClaim:
claimName: mssql-claim
---
apiVersion: v1
kind: Service
metadata:
name: mssql-clusterip-srv
spec:
type: ClusterIP
selector:
app: mssql
ports:
- name: mssql
protocol: TCP
port: 1433
targetPort: 1433
---
apiVersion: v1
kind: Service
metadata:
name: mssql-loadbalancer
spec:
type: LoadBalancer
selector:
app: mssql
ports:
- protocol: TCP
port: 1433
targetPort: 1433
alright, lot's to process here. Let's stop here and when we come back, we'll spint this baby up.
now let's deploy this thing:
kubectl apply -f mssql-plat-depl.yaml
cross your fingers
now lets run this command
kubectl get services
and if we do a
kubectl get pods
here we have an error, so we are going to try to figure out why that is. this is not a bad thing, these yaml files are a bitch.
i verified the yaml file is good, so pretty sure that this is a secrets problem. let's tear down the deployment with
kubectl delete deployment mssql-depl
now let's clear out our secret
kubectl delete secret mssql
and make sure they are cleared
kubectl get secrets
redo our secret
kubectl create secret generic mssql --from-literal=MSSQL_SA_PASSWORD="pa55w0rd!"
redploy our yaml file
kubectl apply -f mssql-plat-depl.yaml
now let's check everything out
kubectl get deployments
looks good, now let's check our pods
kubectl get pods
sweet jesus!!!!!
OK, let's try to login to our sql server. You will need to bring up SQL Server Managment Studio. If you do not have it, you can get it here
try to connect up using this:
the login seems weird, it actually localhost,1433. I think we need to try and play with using different external ports, so we can actually run multiples of these, but for not, this should be find. also, if you are running sql on your local machine, shut it down because they will conflict because of the port. work in progress I guess, but still learning, so this will have to do for now.
we were able to login, but there are no databases yet:
but we can test the persistent storage by creating a database just as a test
now, we can close out of SSMS, and go to Docker Desktop and find our mssql instance and kill it:
shortly after we kill it, it will respawn, and after it comes back, we will log in again and make sure out test database is still there.
yep, still there. let's delete it because we don't need it. in the next branch, we'll update the platform service to actually use this sql server.
let's open back up our PlatformService app and get it ready to connect to our sql server when in production mode. in dev mode, we'll still use the inMemory database though.
open up the appsettings.json file and add this line:
"ConnectionStrings": {
"PlatformsConn": "Server=mssql-clusterip-srv,1433;Initial Catalog=platformsdb;User Id=sa;Password=pa55w0rd!"
}
now we need a conditional statement in our Program.cs to decide which database to use when in development vs production. If you are watching the youtube video, it's using .net5 and we are using .net6 which is a little different, so i had to figure a different way to do this, so, in our Program.cs file, we'll add this code:
if (builder.Environment.IsDevelopment())
{
Console.WriteLine("--> running in developement mode");
builder.Services.AddDbContext<AppDbContext>(opt =>
opt.UseInMemoryDatabase("InMem"));
}
else
{
Console.WriteLine("--> running in production mode");
builder.Services.AddDbContext<AppDbContext>(opt =>
opt.UseSqlServer(builder.Configuration.GetConnectionString("PlatformsConn")));
}
we are going to put that before this line:
builder.Services.AddScoped<IPlatformRepo, PlatformRepo>();
let's test everything out and make sure everybody is happy, so do a dotnet run command and just make sure that you are not getting any errors. after this, we are ready to start setting up some migrations
before we start with migrations, we need to make a small change to our propdb.cs file, so in the DataFolder, open up PropDb.cs, and make this small change:
if (isProd)
{
Console.WriteLine("--> Attempting to apply migrations");
try
{
context.Database.Migrate();
}
catch (Exception ex)
{
Console.WriteLine($"--> could not run migrations: {ex.Message}");
}
}
we are going to have to comment out the line for migration because we are not totally there yet
now, it's time to setup our migrations: open up a command prompt and type
dotnet ef migrations add initialmigration
and we get this lovely message:
there are a couple of work-arounds for this. Basically, this is just telling us that it has no idea what ef it, and this is something that Microsoft changed in .net 6, so let's run these commands:
dotnet new tool-manifest
dotnet tool install --local dotnet-ef --version 6.0.10
let's try the migrations one more time, but this time the command will change a little bit
dotnet dotnet-ef migrations add initialmigration
now you are going to get a bunch of nasty error, so there is still a little work that needs to be done. the problem here is the the inMemory database does not support migrations, so we need to fake things out in order to get this up and running
// if (builder.Environment.IsDevelopment())
// {
// Console.WriteLine("--> running in developement mode");
// builder.Services.AddDbContext<AppDbContext>(opt =>
// opt.UseInMemoryDatabase("InMem"));
// }
// else
// {
Console.WriteLine("--> running in production mode");
builder.Services.AddDbContext<AppDbContext>(opt =>
opt.UseSqlServer(builder.Configuration.GetConnectionString("PlatformsConn")));
// }
basically, we are just telling our app that we are only using a real sql database. and this is just to get our migrations started.
and comment out this line as well
// PrepDb.PrepPopulation(app, app.Environment.IsProduction());
we are also going to need to copy our connection string from the appsettings.json and past it in our appsettings.Development.json, and make a small change
"ConnectionStrings": {
"PlatformsConn": "Server=localhost,1433;Initial Catalog=platformsdb;User Id=sa;Password=pa55w0rd!"
}
that should be enough to get our migrations working
dotnet dotnet-ef migrations add initialmigration
now there should be a new folder called migrations with 3 files in it:
now let's roll back the things that we commented out to get the migrations to work. also dont forget to go back to PrepDb in our Data folder and remove the comment on this line:
context.Database.Migrate();
you may also have to import
using Microsoft.EntityFrameworkCore;
that's quite a chunk, so in the next branch, we'll rebuild our image, and push it back up to docker hub and check to see if our migrations are working or not.
back in our PlatformService, let's do another build
docker build -t c5m7b4/platformservice .
dont forget the period at the end. and then push it up to docker hub
docker push c5m7b4/platformservice
now just double check docker hub and make sure everything is cool.
now we need to update our kubernetes instance so it pulls down our latest version
it doesnt matter what command prompt you do this from, so you don't have to go back to the K8s project to do this.
kubectl get deployments
we want to restart the platforms-depl
kubectl rollout restart deployment platforms-depl
and now if you do
kubectl get pods
you should see everything running. if for some reason, things are out of whack, like maybe you mispelled the password in the config, you can kill the deployment like this
kubectl delete deployment platforms-depl
then fix your errors and redeploy
now, if you look at the logs for the platformservice in docke desktop, you should be able to see the migrations in action:
now just to really check, open up ssms and take a look at our database
now back to insomnia using the K8s version we can go a get all platforms
now let's create a platform
and now recheck our platforms again
now we'll check sql
there you go. we have come a long way so far, but much more to come
now we are going to fledge out our commandsservice to make it more real
so, let's open up the commandsService project and create a new folder called Models
we will create our first model inside that folder called Platform.cs
namespace CommandsService.Models
{
public class Platforms
{
public int Id { get; set; }
public string Name { get; set; }
public int ExternalId { get; set; }
}
}
the next model we will create is our Command.cs file
using System.ComponentModel.DataAnnotations;
namespace CommandsService.Models
{
public class Command
{
[Key]
[Required]
public int Id { get; set; }
[Required]
public string HowTo { get; set; }
[Required]
public string CommandLine { get; set; }
[Required]
public int PlatformId { get; set; }
public Platform Platform { get; set; }
}
}
one thing I'm going to change about this project is in the CommandsService.csproj file
<!-- <Nullable>enable</Nullable> -->
now back over to our Platform class, we will add this:
using System.ComponentModel.DataAnnotations;
namespace CommandsService.Models
{
public class Platform
{
[Key]
[Required]
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public int ExternalId { get; set; }
public ICollection<Command> Commands { get; set; } = new List<Command>();
}
now let's just do a dotnet build to make sure everything is kosher
now we are going to create an in memory database for this for now, so create a new folder called Data and create a new file inside of that called AppDbContext.cs
using CommandsService.Models;
using Microsoft.EntityFrameworkCore;
namespace CommandsService.Data
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> opt) : base(opt)
{
}
public DbSet<Platform> Platforms { get; set; }
public DbSet<Command> Commands { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Platform>()
.HasMany(p => p.Commands)
.WithOne(p => p.Platform!)
.HasForeignKey(p => p.PlatformId);
modelBuilder
.Entity<Command>()
.HasOne(p => p.Platform)
.WithMany(p => p.Commands)
.HasForeignKey(p => p.PlatformId);
}
}
}
now let's just run a dotnet build just to check
now let's create a file in the Data folder called ICommandRepo.cs
using CommandsService.Models;
namespace CommandsService.Data
{
public interface ICommandRepo
{
bool SaveChanges();
IEnumerable<Platform> GetAllPlatforms();
void CreatePlatform(Platform plat);
bool PlatformExists(int platformId);
IEnumerable<Command> GetCommandsForPlatform(int platformId);
Command GetCommand(int platformId, int commandId);
void CreateCommand(int platformId, Command command);
}
}
and now create the concrete class to go with the interface, so create a file called CommandRepo.cs in the Data folder:
using CommandsService.Models;
namespace CommandsService.Data
{
public class CommandRepo : ICommandRepo
{
private readonly AppDbContext _context;
public CommandRepo(AppDbContext context)
{
_context = context;
}
public void CreateCommand(int platformId, Command command)
{
if (command == null)
{
throw new ArgumentNullException(nameof(command));
}
command.PlatformId = platformId;
_context.Commands.Add(command);
}
public void CreatePlatform(Platform plat)
{
if (plat == null)
{
throw new ArgumentNullException(nameof(plat));
}
_context.Platforms.Add(plat);
}
public IEnumerable<Platform> GetAllPlatforms()
{
return _context.Platforms.ToList();
}
public Command GetCommand(int platformId, int commandId)
{
return _context.Commands
.Where(c => c.PlatformId == platformId && c.Id == commandId).FirstOrDefault();
}
public IEnumerable<Command> GetCommandsForPlatform(int platformId)
{
return _context.Commands
.Where(c => c.PlatformId == platformId)
.OrderBy(c => c.Platform.Name);
}
public bool PlatformExists(int platformId)
{
return _context.Platforms.Any(p => p.Id == platformId);
}
public bool SaveChanges()
{
return (_context.SaveChanges() >= 0);
}
}
}
now we are just going to run a dotnet build just to check
now we are going to create some Dtos, so create a folder in the root of the project called Dtos. let's create our first file, PlatformReadDto.cs
namespace CommandsService.Dtos
{
public class PlatformReadDto
{
public int Id { get; set; }
public string Name { get; set; }
}
}
now lets create the CommandReadDto.cs
namespace CommandsService.Dtos
{
public class CommandReadDto
{
public int Id { get; set; }
public string HowTo { get; set; }
public string CommandLine { get; set; }
public int PlatformId { get; set; }
}
}
now we are going to create one called CommandCreateDto.cs
using System.ComponentModel.DataAnnotations;
namespace CommandsService.Dtos
{
public class CommandCreateDto
{
[Required]
public string HowTo { get; set; }
[Required]
public string CommandLine { get; set; }
}
}
now it's time for automapper
let's open up the Program.cs file and add this under AddControllers
builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
now we create the folder called Profiles and create a file in there called CommandsProfile.cs
using AutoMapper;
using CommandsService.Dtos;
using CommandsService.Models;
namespace CommandsService.Profiles
{
public class CommandsProfile : Profile
{
public CommandsProfile()
{
// source => target
CreateMap<Platform, PlatformReadDto>();
CreateMap<CommandCreateDto, Command>();
CreateMap<Command, CommandReadDto>();
}
}
}
that's it for our automapper
still inside of the CommandsService project, let's open up the PlatformsController.cs. let's make these changes to it
private readonly ICommandRepo _repo;
private readonly IMapper _mapper;
public PlatformsController(ICommandRepo repo, IMapper mapper)
{
_repo = repo;
_mapper = mapper;
}
we are missing a major piece though. we need to setup our dependency injection for our repo. so back in program.cs, we need to add this:
builder.Services.AddDbContext<AppDbContext>(opt => opt.UseInMemoryDatabase("InMem"));
builder.Services.AddScoped<ICommandRepo, CommandRepo>();
let make sure that it builds by running dotnet build
now let's go back to the PlatformsController.cs and add another endpoint
[HttpGet]
public ActionResult<IEnumerable<PlatformReadDto>> GetPlatforms()
{
Console.WriteLine("--> Getting Platforms from CommandsService");
var platformItems = _repo.GetAllPlatforms();
return Ok(_mapper.Map<IEnumerable<PlatformReadDto>>(platformItems));
}
let's give this a go, so we'll spin it up by running dotnet run
let's go over to insomnia and create a test for that endpoint
looks good. we wouldn't expect to have any data in the inmemory database that we just spun up.
let's create our second controller called CommandsController.cs
using AutoMapper;
using CommandsService.Data;
using CommandsService.Dtos;
using Microsoft.AspNetCore.Mvc;
namespace CommandsService.Controllers
{
[Route("api/c/platforms/{platformId}/[controller]")]
[ApiController]
public class CommandsController : ControllerBase
{
private readonly ICommandRepo _repo;
private readonly IMapper _mapper;
public CommandsController(ICommandRepo repo, IMapper mapper)
{
_repo = repo;
_mapper = mapper;
}
[HttpGet]
public ActionResult<IEnumerable<CommandReadDto>> GetCommandsForPlatform(int platformId)
{
Console.WriteLine($"--> Hit GetCommandsForPlatform: {platformId}");
if (!_repo.PlatformExists(platformId))
{
return NotFound();
}
var commands = _repo.GetCommandsForPlatform(platformId);
return Ok(_mapper.Map<IEnumerable<CommandReadDto>>(commands));
}
}
}
we can start with this, but if we try to test it, we aren't going to get anything back, but that's ok for now.
lets do a dotnet run and head back to insomnia
this looks good for now, we are not expecting anything back from this just yet.
let's add another endpoint
[HttpGet("{commandId}", Name = "GetCommandForPlatform")]
public ActionResult<CommandReadDto> GetCommandForPlatform(int platformId, int commandId)
{
Console.WriteLine($"--> Hit GetCommandForPlatform: {platformId} / {commandId}");
if (!_repo.PlatformExists(platformId))
{
return NotFound();
}
var command = _repo.GetCommand(platformId, commandId);
if (command == null)
{
return NotFound();
}
return Ok(_mapper.Map<CommandReadDto>(command));
}
we can spin it up and make sure that the endpoint is working so, do a dotnet run
we still get a not found
but if we check out log, we can see the results there
and now for our final route
[HttpPost]
public ActionResult<CommandReadDto> CreateCommandForPlatform(int platformId, CommandCreateDto commandDto)
{
Console.WriteLine($"--> Hit CreateCommandForPlatform: {platformId}");
if (!_repo.PlatformExists(platformId))
{
return NotFound();
}
var command = _mapper.Map<Command>(commandDto);
_repo.CreateCommand(platformId, command);
_repo.SaveChanges();
var commandReadDto = _mapper.Map<CommandReadDto>(command);
return CreatedAtRoute(nameof(GetCommandForPlatform), new { platformId = platformId, commandId = commandReadDto.Id }, commandReadDto);
}
spin up a dotnet run and create this in insomnia
and also make sure that we can see our log because we are just going to get a 404 not found
now we are going to look into setting up rabbitmq as our messaging but. I need to do more research on this because there are features that I would like to learn. one thing to lookup also is amqp (advanced-messaging-queuing-protocal)
there are 4 types of exchange
- Direct
- Fanout
- Topic
- Header
so, let's go to our K8S project and create a new file called rabbitmq-depl.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: rabbitmq-depl
spec:
replicas: 1
selector:
matchLabels:
app: rabbitmq
template:
metadata:
labels:
app: rabbitmq
spec:
containers:
- name: rabbitmq
image: rabbitmq:3-management
ports:
- containerPort: 15672
name: rbmq-mgmt-port
- containerPort: 5672
name: rbmq-msg-port
---
apiVersion: v1
kind: Service
metadata:
name: rabbitmq-clusterip-srv
spec:
type: ClusterIP
selector:
app: rabbitmq
ports:
- name: rbmq-mgmt-port
protocol: TCP
port: 15672
targetPort: 15672
- name: rbmq-msg-port
protocol: TCP
port: 5672
targetPort: 5672
---
apiVersion: v1
kind: Service
metadata:
name: rabbitmq-loadbalancer
spec:
type: LoadBalancer
selector:
app: rabbitmq
ports:
- name: rbmq-mgmt-port
protocol: TCP
port: 15672
targetPort: 15672
- name: rbmq-msg-port
protocol: TCP
port: 5672
targetPort: 5672
now lets run the deployment
kubectl apply -f rabbitmq-depl.yaml
now let's get our deployments
kubectl get deployments
it might take some time to start. notice in my screenshot, it is not started yet, so let's check out pods
kubectl get pods
looking good now.
now let's open up a browser and go go localhost:15672
you can login with guest/guest
now let's go into our platform service
we need to add a dependency for RabbitMQ
dotnet add package RabbitMQ.Client
now in our appsettings.Development.json we need to add our config
"RabbitMQHost": "localhost",
"RabbitMQPort": "5672"
and in our appsettings.json, we need to add that config
"RabbitMQHost": "rabbitmq-clusterip-srv",
"RabbitMQPort": "5672"
now let's create a dto, so in the Dtos folder create a file named PlatformPublishedDto.cs
namespace PlatformService.Dtos
{
public class PlatformPublishedDto
{
public int Id { get; set; }
public string Name { get; set; }
public string Event { get; set; }
}
}
now we need to create another mapping, so let's go into the Profiles folder and update that file:
CreateMap<PlatformReadDto, PlatformPublishedDto>();
now let's create a folder in the root of our project and name it AsyncDataServices and create a file in there called IMessageBusClient
using PlatformService.Dtos;
namespace PlatformService.AsyncDataService
{
public interface IMessageBusClient
{
void PublishNewPlatform(PlatformPublishedDto platformPublishedDto);
}
}
now we are going to implement this interface so create a file calledMessageBusClient.cs. this is going to be kind of a lengthy file
using System.Text;
using System.Text.Json;
using PlatformService.Dtos;
using RabbitMQ.Client;
namespace PlatformService.AsyncDataService
{
public class MessageBusClient : IMessageBusClient
{
private readonly IConfiguration _config;
private readonly IConnection _connection;
private readonly IModel _channel;
public MessageBusClient(IConfiguration config)
{
_config = config;
var factory = new ConnectionFactory() { HostName = _config["RabbitMQHost"], Port = int.Parse(_config["RabbitMQPort"]) };
try
{
_connection = factory.CreateConnection();
_channel = _connection.CreateModel();
_channel.ExchangeDeclare(exchange: "trigger", type: ExchangeType.Fanout);
_connection.ConnectionShutdown += RabbitMQ_ConnectionShutdown;
Console.WriteLine("--> Connected to Message Bus");
}
catch (System.Exception ex)
{
Console.WriteLine($"--> Could not connect to the Message Bus: {ex.Message}");
}
}
private void RabbitMQ_ConnectionShutdown(object sender, ShutdownEventArgs e)
{
Console.WriteLine("--> Message Bus has disconnected");
}
private void SendMessage(string message)
{
var body = Encoding.UTF8.GetBytes(message);
_channel.BasicPublish(exchange: "trigger", routingKey: "", basicProperties: null, body: body);
Console.WriteLine($"--> We have sent {message}");
}
public void PublishNewPlatform(PlatformPublishedDto platformPublishedDto)
{
var message = JsonSerializer.Serialize(platformPublishedDto);
if (_connection.IsOpen)
{
Console.WriteLine("--> Rabbit MQ Connection open, sending message...");
SendMessage(message);
}
else
{
Console.WriteLine("--. Rabbit MQ connection is closed, not sendind");
}
}
public void Dispose()
{
Console.WriteLine("Message Bus Disposed");
if (_channel.IsOpen)
{
_channel.Close();
_connection.Close();
}
}
}
}
lets do a dotnet build just to make sure we haven't broken anything
now we just need to register our dependency injection
builder.Services.AddSingleton<IMessageBusClient, MessageBusClient>();
after you commit, you may have some nasty errors, so use ctrl-shift-p and type reload to fix things up.
now let's go into the PlatformsController and setup our Dependency Injection
private readonly IMapper _mapper;
private readonly ICommandDataClient _commandDataClient;
private readonly IMessageBusClient _messageBusClient;
private readonly IPlatformRepo _repo;
public PlatformsController(IPlatformRepo repo, IMapper mapper, ICommandDataClient commandDataClient, IMessageBusClient messageBusClient)
{
_repo = repo;
_mapper = mapper;
_commandDataClient = commandDataClient;
_messageBusClient = messageBusClient;
}
and then in our post, we'll add this code
try
{
var platforPublishedDto = _mapper.Map<PlatformPublishedDto>(platformReadDto);
platforPublishedDto.Event = "Platform_Published";
_messageBusClient.PublishNewPlatform(platforPublishedDto);
}
catch (System.Exception ex)
{
Console.WriteLine($"--> Could not send asynchronously: {ex.Message}");
}
now let's fire up our PlatformService with a dotnet run
now let's open up the CommandService and do a dotnet run on it
then we'll go to insomnia and on our local platform service, make sure we can still get all platforms and then let's create a platform
then in our command service, we should still see this
and i our PlatformService we should see our new logs
now if we blast the Create command, we can see in rabbitmq, our activity
now let's open up teh ComandsService and work on that for a little while.
first we need to add the RabbitMQ.Client dependency
dotnet add package RabbitMQ.Client
make sure the check the CommandsService.csproj to make sure it got loaded.
now in our appsettings.Development.json, let's add our RabbitMQ config
"RabbitMQHost": "localhost",
"RabbitMQPort": "5672"
then i nour appsettings.json, we need to add the config there as well
"RabbitMQHost": "rabbitmq-clusterip-srv",
"RabbitMQPort": "5672"
now we are going to create some Dtos, so create a file in the Dto folder called PlatformPublishedDto.cs
namespace CommandsService.Dtos
{
public class PlatformPublishedDto
{
public int Id { get; set; }
public string Name { get; set; }
public string Event { get; set; }
}
}
now create another dto called GenericEventDto.cs
namespace CommandsService.Dtos
{
public class GenericEventDto
{
public string Event { get; set; }
}
}
now we need to change our profile code to AutoMapper
CreateMap<PlatformPublishedDto, Platform>()
.ForMember(dest => dest.ExternalId, opt => opt.MapFrom(src => src.Id));
now i our ICommandRepo.cs file, add this little snippet
bool ExternalPlatformExists(int externalPlatformId);
and then we need to implement this in the CommandRepo.cs file
public bool ExternalPlatformExists(int externalPlatformId)
{
return _context.Platforms.Any(p => p.ExternalId == externalPlatformId);
}
let's create a root folder in our CommandsService project called EventProcessing.
inside of there, create a file called IEventProcessor.cs
namespace CommandsService.EventProcessing
{
public interface IEventProcessor
{
void ProcessEvent(string message);
}
}
then create another file called EventProcessor.cs
using System.Text.Json;
using AutoMapper;
using CommandsService.Data;
using CommandsService.Dtos;
using CommandsService.Models;
namespace CommandsService.EventProcessing
{
public class EventProcessor : IEventProcessor
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly IMapper _mapper;
public EventProcessor(IServiceScopeFactory scopeFactory, IMapper mapper)
{
_scopeFactory = scopeFactory;
_mapper = mapper;
}
public void ProcessEvent(string message)
{
var eventType = DetermineEvent(message);
switch (eventType)
{
case EventType.PlatformPublished:
// To Do
break;
default:
break;
}
}
private EventType DetermineEvent(string notificationMessage)
{
Console.WriteLine("--> Determining Event Type");
var eventType = JsonSerializer.Deserialize<GenericEventDto>(notificationMessage);
switch (eventType.Event)
{
case "Platform_Published":
Console.WriteLine("--> Platform Published Event detected");
return EventType.PlatformPublished;
default:
Console.WriteLine("--> Could not determine event type");
return EventType.Undetermined;
}
}
private void AddPlatform(string platformPublishedMessage)
{
using (var scope = _scopeFactory.CreateScope())
{
var repo = scope.ServiceProvider.GetRequiredService<ICommandRepo>();
var platformPublishedDto = JsonSerializer.Deserialize<PlatformPublishedDto>(platformPublishedMessage);
try
{
var plat = _mapper.Map<Platform>(platformPublishedDto);
if (!repo.ExternalPlatformExists(plat.ExternalId))
{
repo.CreatePlatform(plat);
repo.SaveChanges();
}
else
{
Console.WriteLine("--> Platform already exitss");
}
}
catch (System.Exception ex)
{
Console.WriteLine($"--> Could not add platform to teh db {ex.Message}");
}
}
}
}
enum EventType
{
PlatformPublished,
Undetermined
}
}
i know, this one is big
now in our Program.cs file, we need to register our dependency injection
builder.Services.AddSingleton<IEventProcessor, EventProcessor>();
let's quickly do a build with dotnet build
and let's test a dotnet run, just to check
now let's create another folder in the root of the CommandsService project called AsyncDataServices
now in that folder, create a file called MessageBusSubscriber.cs
using System.Text;
using CommandsService.EventProcessing;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
namespace CommandsService.AsyncDataServices
{
public class MessageBusSubscriber : BackgroundService
{
private readonly IConfiguration _config;
private readonly IEventProcessor _eventProcessor;
private IConnection _connection;
private IModel _channel;
private string _queueName;
public MessageBusSubscriber(IConfiguration config, IEventProcessor eventProcessor)
{
_config = config;
_eventProcessor = eventProcessor;
InitializeRabbitMQ();
}
private void InitializeRabbitMQ()
{
var factory = new ConnectionFactory() { HostName = _config["RabbitMQHost"], Port = int.Parse(_config["RabbitMQPort"]) };
_connection = factory.CreateConnection();
_channel = _connection.CreateModel();
_channel.ExchangeDeclare(exchange: "trigger", type: ExchangeType.Fanout);
_queueName = _channel.QueueDeclare().QueueName;
_channel.QueueBind(queue: _queueName, exchange: "trigger", routingKey: "");
Console.WriteLine("--> Listening on the Message Bus");
_connection.ConnectionShutdown += RabbitMQ_ConnectionShutdown;
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
// do this one last
stoppingToken.ThrowIfCancellationRequested();
var consumer = new EventingBasicConsumer(_channel);
consumer.Received += (ModuleHandle, ea) =>
{
Console.WriteLine("--> Event received");
var body = ea.Body;
var notificationMessage = Encoding.UTF8.GetString(body.ToArray());
_eventProcessor.ProcessEvent(notificationMessage);
};
_channel.BasicConsume(queue: _queueName, autoAck: true, consumer: consumer);
return Task.CompletedTask;
}
private void RabbitMQ_ConnectionShutdown(object sender, ShutdownEventArgs e)
{
Console.WriteLine("--> Connection shutdown");
}
public override void Dispose()
{
if (_channel.IsOpen)
{
_channel.Close();
_connection.Close();
}
base.Dispose();
}
}
}
now we just need to register this, so open up the Program.cs file and add this after the controllers
builder.Services.AddHostedService<MessageBusSubscriber>();
now, we are ready to test
let's open up the platform service and do a dotnet build and dotnet run
then go back to the command service and do a dotnet build and a dotnet run
this is the important part
now over to insomnia and we are using the local versions for testing. make sure first that the Get all Platforms is still working, and then we are going to Create a platform and see what happens.
in our command service, we should see this:
and in our platform service, we should see this
we are missing something though, in the command service, open uup the EventProcessor class and let's first add a console message on lin 66 after we save the
then in our ToDo section that we left around line 25, we need to add this:
switch (eventType)
{
case EventType.PlatformPublished:
AddPlatform(message);
break;
default:
break;
}
now we can restart the command service and test Creating a platform again with insomnia
now in our command service, we'll get some better logging:
now if we go to the CommandService in insomnia, we should be able to run the Get all platforms there and we should see the platform we added
now let's try the Create Command for Platform. note the id that was returned
actually, i messed that up. I need to the use id that that is in the previous screenshot
and now we should be able to run the Get all Commands for Platform
congrats, this was all a big step. next up, let's rebuild everthing and re-push our images to docker hub and refresh kubernetes and do some more testing
let's go back over to our platform service, shut everthing down and do a fresh docker build
docker build -t c5m7b4/platformservice .
and now we will push it back up to docker hub
docker push c5m7b4/platformservice
now we need to go over to our CommandService and do a build there as well
docker build -t c5m7b4/commandservice .
and then push that up to docker hub
docker push c5m7b4/commandservice
check our work on docker hub of course
next up, let's double check our deployments
kubectl get deployments
now lets restart the platform-depl
kubectl rollout restart deployment platforms-depl
make sure the pods are running
kubectl get pods
now we'll go over to insomnia and using our K8s Platform service let's create a platform and see what we get
now if we do a get all platforms
all is looking good. let check the logs
now let's restart the CommandService
kubectl rollout restart deployment commands-depl
make sure the pods are running
kubectl get pods
now we'll create another platform
and take a look at the logs
now lets create a CommandService folder in our K8s insomnia folder and create the Get all Platforms request
in the next section we will look at grpc and really syncing our commands
in order to get grpc working, we are going to have to modify our platforms-depl.yaml file, so let's open up the K8s project and open that platforms-depl.yaml file
we just need to add one more port
- name: platformgrpc
protocol: TCP
port: 666
targetPort: 666
redeploy our service
kubectl apply -f platforms-depl.yaml
kubectl get services
now you will notice the 666 port is present
now let's go back to the platform service because it is going to be the grpc server
now in our appsettings.json, we need to add this little bit of config ***** on a side note, i'm pretty sure that this has to go into appsetting.Production.json or we will get a clash of port 80 in developement mode.
"Kestrel": {
"Endpoints": {
"Grpc":{
"Protocols": "Http2",
"Url":"http://platforms-clusterip-srv:666"
},
"webApi":{
"Protocols": "Http1",
"Url":"http://platforms-clusterip-srv:80"
}
}
}
we do need to add one package to our Platform service
dotnet add package Grpc.AspNetCore
now we are going to jump back over to our command service and add the needed packages there
dotnet add package Grpc.Tools
dotnet add package Grpc.Net.Client
dotnet add package Google.Protobuf
now let's go back into our PlatformService and create the proto file let's create a folder in the root called Protos and we are going to create a file in there called platforms.proto
syntax = "proto3";
option csharp_namespace = "PlatformService";
service GrpcPlatform {
rpc GetAllPlatforms (GetAllRequests) returns (PlatformResponse);
}
message GetAllRequests {}
message GrpcPlatformModel{
int32 platformId = 1;
string name = 2;
string publisher = 3;
}
message PlatformResponse{
repeated GrpcPlatformModel platform = 1;
}
this one is a bit strange
now we need to go to the csproj file and add this
<ItemGroup>
<Protobuf Include="Protos\platforms.proto" GrpcServices="Server" />
</ItemGroup>
now let's do a dotnet build
now if we need in obj\Debug\Protos we will see the autogenerated code
now let's create a folder inside our SyncDataServices folder called Grpc and create a file in there called GrpcPlatformService.cs
namespace PlatformService.SyncDataServices.Grpc
{
public class GrpcPlatformService : GrpcPlatform.GrpcPlatformBase
{
}
}
but before we can finish this file, we need to go back into the Profiles folder and add another map
CreateMap<Platform, GrpcPlatformModel>()
.ForMember(dest => dest.PlatformId, opt => opt.MapFrom(src => src.Id))
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name))
.ForMember(dest => dest.Publisher, opt => opt.MapFrom(src => src.Publi
now let's go back into the GrpcPlatformService.cs file
using AutoMapper;
using Grpc.Core;
using PlatformService.Data;
namespace PlatformService.SyncDataServices.Grpc
{
public class GrpcPlatformService : GrpcPlatform.GrpcPlatformBase
{
private readonly IPlatformRepo _repo;
private readonly IMapper _mapper;
public GrpcPlatformService(IPlatformRepo repo, IMapper mapper)
{
_repo = repo;
_mapper = mapper;
}
public override Task<PlatformResponse> GetAllPlatforms(GetAllRequests request, ServerCallContext context)
{
var response = new PlatformResponse();
var platforms = _repo.GetAllPlatforms();
foreach (var plat in platforms)
{
response.Platform.Add(_mapper.Map<GrpcPlatformModel>(plat));
}
return Task.FromResult(response);
}
}
}
now in our Progam.cs file, we need to register Grpc right below our singleton
builder.Services.AddGrpc();
then at the bottom of our Program.cs just before the app.Run, add these lines
app.MapGrpcService<GrpcPlatformService>();
app.MapGet("/protos/platforms.proto", async context =>
{
await context.Response.WriteAsync(File.ReadAllText("Protos/platforms.prot"));
});
now let's do a dotnet build
now let's try a dotnet run
we skipped branch46
now we move on to the Commandservice
in the appsettings.Development.json, add this config
"GrpcPlatform": "https://localhost:5001"
and then in the production settings
"GrpcPlatform": "http://platforms-clust:666"
let's make a folder in the root now called Protos and copy the file from the PlatformService project into this one
we are also going to copy the ItemGroup additions from the PlatformService found in teh csproj file with one change - Client mode
<ItemGroup>
<Protobuf Include="Protos\platforms.proto" GrpcServices="Client" />
</ItemGroup>
lets do a dotnet build
in the root, create a folder called SyncDataService and then another folder called Grpc and then inside of that a file called IPlatformDataClient.cs
using CommandsService.Models;
namespace CommandsService.SyncDataServices.Grpc
{
public interface IPlatformDataClient
{
IEnumerable<Platform> ReturnAllPlatforms();
}
}
then create another file in the Grpc folder called PlatformDataClient.cs
using AutoMapper;
using CommandsService.Models;
namespace CommandsService.SyncDataServices.Grpc
{
public class PlatformDataClient : IPlatformDataClient
{
private readonly IConfiguration _config;
private readonly IMapper _mapper;
public PlatformDataClient(IConfiguration config, IMapper mapper)
{
_config = config;
_mapper = mapper;
}
public IEnumerable<Platform> ReturnAllPlatforms()
{
throw new NotImplementedException();
}
}
}
at this point, let's set up the automapper so go into Profiles
CreateMap<GrpcPlatformModel, Platform>()
.ForMember(dest => dest.ExternalId, opt => opt.MapFrom(src => src.PlatformId))
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name))
.ForMember(dest => dest.Commands, opt => opt.Ignore());
now ack to our PlatformDataClient.cs file
using AutoMapper;
using CommandsService.Models;
using Grpc.Net.Client;
using PlatformService;
namespace CommandsService.SyncDataServices.Grpc
{
public class PlatformDataClient : IPlatformDataClient
{
private readonly IConfiguration _config;
private readonly IMapper _mapper;
public PlatformDataClient(IConfiguration config, IMapper mapper)
{
_config = config;
_mapper = mapper;
}
public IEnumerable<Platform> ReturnAllPlatforms()
{
Console.WriteLine($"--> Calling Grpc Service: {_config["GrpcPlatform"]}");
var channel = GrpcChannel.ForAddress(_config["GrpcPlatform"]);
var client = new GrpcPlatform.GrpcPlatformClient(channel);
var request = new GetAllRequests();
try
{
var reply = client.GetAllPlatforms(request);
return _mapper.Map<IEnumerable<Platform>>(reply.Platform);
}
catch (System.Exception ex)
{
Console.WriteLine($"--> Could not call Grpc Server: {ex.Message}");
return null;
}
}
}
}
now in our Program.cs file, we need to add this
builder.Services.AddScoped<IPlatformDataClient, PlatformDataClient>();
dotnet build dotnet run
now in the Data folder, create a file called PrepDb.cs
using CommandsService.Models;
using CommandsService.SyncDataServices.Grpc;
namespace CommandsService.Data
{
public static class PrepDb
{
public static void PrepPopulation(IApplicationBuilder builder)
{
using (var serviceScope = builder.ApplicationServices.CreateScope())
{
var grpcClient = serviceScope.ServiceProvider.GetService<IPlatformDataClient>();
var platforms = grpcClient.ReturnAllPlatforms();
SeedData(serviceScope.ServiceProvider.GetService<ICommandRepo>(), platforms);
}
}
private static void SeedData(ICommandRepo repo, IEnumerable<Platform> platforms)
{
Console.WriteLine("Seeding new platforms");
foreach (var plat in platforms)
{
if (!repo.ExternalPlatformExists(plat.ExternalId))
{
repo.CreatePlatform(plat);
}
repo.SaveChanges();
}
}
}
}
now in startup, we just need to add this one last line
PrepDb.PrepPopulation(app);
we are back in the platform service again and do a dotnet run
now lets just check insomnia to make sure there is some data in there.
now we can go over to the comands servic and run dotnet run and when it starts up, it should use grpc to pull data from the PlatformService
head back over to the platformservice
docker build -t c5m7b4/platformservice .
docker push c5m7b4/platformservice
now back over to the CommandService
docker build -t c5m7b4/commandservice .
docker push c5m7b4/commandservice
let go back over to the PlatformService
kubectl get deployments
kubectl rollout restart deployment platforms-depl
kubectl get pods
make sure all pods are running and check the logs after it starts up probably wouldnt hurt to check insomnia also to make sure we can get all platforms
kubectl rollout restart deployment commands-depl
check the pods check the logs
this one actually fails, so i need to do some more research
actually if we go back into the appsettings.json for the CommandServer, we can fix the address:
"GrpcPlatform": "http://platforms-clusterip-srv:666"
still inside of the CommandService
docker build -t c5m7b4/commandservice .
docker push c5m7b4/commandservice
check out deployments and restart the commandservice
kubectl rollout restart deployment commands-depl
congrats for making it this far.
more food for thought:
- https/tls
- Event Process
- Service Discovery
- Bearer Authentication
- refactor endpoint return types