This exercise will cover the following:
- Building a simple web service with .Net core
- Deploying the service to PCF
- Using Steeltoe to bind to a redis cloud instance
- Doing a blue/green deployment with PCF
- Install the .Net core SDK from this URL: https://dotnet.microsoft.com/download
- Verify the install by opening a terminal or command window and typing
dotnet --version. You should see a version string to match the version you installed
- Install the Cloud Foundry CLI from this URL: https://docs.cloudfoundry.org/cf-cli/install-go-cli.html
- Verify the install by opening a terminal or command window and typing
cf --version. You should see a version string to match the version you installed
- Install either Visual Studio IDE or Visual Studio Code from this URL: https://visualstudio.microsoft.com/
- If you choose Visual Studio Code, also install the C# extension
If you are using a private installation of PCF, then obtain credentials and API enpoint information from your PCF platform team. If you are using Pivotal Web Services (the public PCF instance hosted by Pivotal), then go to https://run.pivotal.io/ and register for a free account.
Once you have credentials, login with the CLI...
- Open a terminal or command window and login to PCF with the command
cf login -a api.run.pivotal.io(or whatever API endpoint you are using if not Pivotal Web Services) - Enter the email you registered and the password you set
-
Create a basic web service project and open it in VS Code
mkdir PaymentService cd PaymentService dotnet new webapi dotnet add package Swashbuckle.AspNetCore dotnet add package Microsoft.Extensions.Caching.Redis dotnet add package Steeltoe.Management.CloudFoundryCore dotnet add package Steeltoe.Extensions.Configuration.CloudFoundryCore dotnet add package Steeltoe.CloudFoundry.ConnectorCore dotnet add package Steeltoe.Extensions.Logging.DynamicLogger code .
-
Run the new web service with
dotnet run(or just press F5 in Visual Studio/Code), then navigate to https://localhost:5001/api/values -
Stop the service with
ctrl-c(or press the stop button in Visual Studio/Code)
-
Create a new
Servicesdirectory -
Create a new class
PaymentServicein theServicesdirectory, set its contents to the following:using System; namespace PaymentService.Services { public class PaymentCalculator { public Decimal Calculate(double Amount, double Rate, int Years) { if (Rate == 0.0) { return CalculateWithoutInterest(Amount, Years); } else { return CalculateWithInterest(Amount, Rate, Years); } } private Decimal CalculateWithInterest(double Amount, double Rate, int Years) { double monthlyRate = Rate / 100.0 / 12.0; int numberOfPayments = Years * 12; double payment = (monthlyRate * Amount) / (1.0 - Math.Pow(1.0 + monthlyRate, -numberOfPayments)); return ToMoney(payment); } private Decimal CalculateWithoutInterest(double Amount, int Years) { int numberOfPayments = Years * 12; return ToMoney(Amount / numberOfPayments); } private Decimal ToMoney(double d) { Decimal bd = new Decimal(d); return Decimal.Round(bd, 2, MidpointRounding.AwayFromZero); } } }
-
Create a new interface
IHitCountServicein theServicesdirectory, set its contents to the following:namespace PaymentService.Services { public interface IHitCountService { long GetAndIncrement(); void Reset(); } }
-
Create a new Class
MemoryHitCountServicein theServicesdirectory, set its contents to the following:namespace PaymentService.Services { public class MemoryHitCountService: IHitCountService { private long HitCount = 0; public long GetAndIncrement() { return ++HitCount; } public void Reset() { HitCount = 0; } } }
-
Create a new
Modelsdirectory -
Create a new class
CalculatedPaymentin theModelsdirectory, set its contents to the following:namespace PaymentService.Models { public class CalculatedPayment { public double Amount {get; set;} public double Rate {get; set;} public int Years {get; set;} public decimal Payment {get; set;} public string Instance {get; set;} public long Count {get; set;} } }
-
Create a new class
PaymentControllerin theControllersdirectory, set its contents to the following:using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using PaymentService.Models; using PaymentService.Services; using Steeltoe.Extensions.Configuration.CloudFoundry; namespace PaymentService.Controllers { [Route("/[controller]")] [ApiController] public class PaymentController { private PaymentCalculator PaymentCalculator; private CloudFoundryApplicationOptions AppOptions; private IHitCountService HitCountService; private readonly ILogger _logger; public PaymentController(PaymentCalculator paymentCalculator, IOptions<CloudFoundryApplicationOptions> appOptions, IHitCountService hitCountService, ILogger<PaymentController> logger) { PaymentCalculator = paymentCalculator; AppOptions = appOptions.Value; HitCountService = hitCountService; _logger = logger; } [HttpGet] public ActionResult<CalculatedPayment> calculatePayment(double Amount, double Rate, int Years) { var Payment = PaymentCalculator.Calculate(Amount, Rate, Years); _logger.LogDebug("Calculated payment of {Payment} for input amount: {Amount}, rate: {Rate}, years: {Years}", Payment, Amount, Rate, Years); return new CalculatedPayment { Amount = Amount, Rate = Rate, Years = Years, Instance = AppOptions.InstanceIndex.ToString(), Count = HitCountService.GetAndIncrement(), Payment = Payment }; } } }
-
Modify
Startup.csby adding the following lines at the end of theConfigureServicesmethod:services.AddCors(); services.AddOptions(); services.ConfigureCloudFoundryOptions(Configuration); services.AddSingleton<PaymentCalculator>(); services.AddSingleton<IHitCountService, MemoryHitCountService>();
-
Modify
Startup.csby adding the following line in theConfiguremethod prior to the existing lineapp.UseHttpsRedirection();:app.UseCors(builder => builder.AllowAnyOrigin());
-
Modify
Program.csby modifying theCreateWebHostBuildermethod so that it looks like this:public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .UseCloudFoundryHosting() .AddCloudFoundry();
-
Start the application either with the debugger (F5), or by entering the command
dotnet run -
Try the web service with the URL https://localhost:5001/payment?amount=100000&rate=4.5&years=30
-
Verify that a payment of $506.69 is returned
Swagger is a REST documentation and UI tool, that also includes code generation tools for clients. For us, it will act as a very simple and almost free UI for the web service we've just created. There are two implementations of swagger: Swashbuckle and NSwag. For this exercise, we will use Swashbuckle.
-
Modify
Startup.cs, add the following to the end of theConfigureServicesmethod:services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); });
-
Modify
Startup.cs, add the following to the end of theConfiguremethod before theUseMvcmiddleware:app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); c.RoutePrefix = string.Empty; });
-
Start the application. The swagger UI should now be available at the application root (https://localhost:5001)
-
Notice that Swagger has documented both web services - the payment calculator we wrote as well as the default service generated by dotnet. If you want, you can delete the generated web service by deleting
ValuesController.cs
-
Create a file
manifest.ymlin the project root directory. Set its contents to the following:applications: - name: PaymentService-1.0 random-route: true
-
cf push
During the push process, PCF will create a route for the app. Make note of the route - you can access the application at this URL once the application has started.
-
Modify
Startup.cs, add the following to the end of theConfigureServicesmethod:services.AddCloudFoundryActuators(Configuration);
-
Modify
Startup.cs, add the following toConfiguremethod before theapp.UseCors()call:app.UseCloudFoundryActuators();
-
Modify
Program.cs, add the following to theCreateWebHostBuilderafter theUseStartupcall:.UseStartup<Startup>() // existing .AddCloudFoundryHosting() //existing .AddCloudFoundry() //existing .ConfigureLogging((builderContext, loggingBuilder) => { loggingBuilder.AddConfiguration(builderContext.Configuration.GetSection("Logging")); loggingBuilder.AddDynamicConsole(); loggingBuilder.AddDebug(); });
You may need to manually add the following
usingstatements:using Microsoft.Extensions.Logging; using Steeltoe.Extensions.Logging;
-
Modify
appsettings.jsonand add the following:"info": { "app": { "name": ".Net Core Payment Calculator" } }
-
Start the application. You should be able to access the management enpoints at https://localhost:5001/actuator
-
Create a new file
appsettings.Production.jsonin the project root directory. Set the contents to the following:{ "management": { "endpoints": { "path": "/cloudfoundryapplication", "cloudfoundry": { "validateCertificates": false } } } } -
cf push -
Test the management endpoints in PCF application manager
-
Create an Redis service instance in your PCF space. Name the service instance
xxxrediswhere "xxx" is your initials- On Pivotal Web Services, add a Redis Cloud instance using the 30MB (free) plan
-
If you are using Azure Redis Cache through the Azure Service Broker on PCF, then modify
appsettings.jsonand add the following configuration:"redis": { "client": { "urlEncodedCredentials": true } }
-
Create a class
RedisHitCountServicein theServicesfolder. Set its contents to the following:using StackExchange.Redis; namespace PaymentService.Services { public class RedisHitCountService: IHitCountService { private IConnectionMultiplexer _conn; public RedisHitCountService (IConnectionMultiplexer conn) { _conn = conn; } public long GetAndIncrement() { IDatabase db = _conn.GetDatabase(); return db.StringIncrement("loan-calculator"); } public void Reset() { IDatabase db = _conn.GetDatabase(); db.StringSet("loan-calculator", 5000); } } }
-
Modify the constructor in
Startup.csto accept and keep theIHostingEnvironment. The modified constructor and new instance variable look like this:public Startup(IConfiguration configuration, IHostingEnvironment env) { Configuration = configuration; Env = env; } public IHostingEnvironment Env {get; }
-
Change the
ConfigureServicesmethod so that the memory based hit counter is used in the development environment and the Redis based hit counter is used in other environments:if (Env.IsDevelopment()) { services.AddSingleton<IHitCountService, MemoryHitCountService>(); } else { services.AddRedisConnectionMultiplexer(Configuration); services.AddSingleton<IHitCountService, RedisHitCountService>(); }
-
Modify
manifest.ymlto add the service binding (change the app version, and specify the correct name of the redis instance you created above):applications: - name: PaymentService-1.1 random-route: true services: - xxxredis
-
cf push
-
Create a new route with the cf cli:
cf create-route dev apps.pcfpoc.jgbpcf.com --hostname xxx-loancalculator
Where xxx is your initials
-
Assign the route to version 1.0 of the app:
cf map-route NetLoanCalculator-1.0 apps.pcfpoc.jgbpcf.com --hostname xxx-loancalculator
-
Verify that the app responds to the new URL.
-
Assign the route to version 1.1 of the app:
cf map-route NetLoanCalculator-1.1 apps.pcfpoc.jgbpcf.com --hostname xxx-loancalculator
Where xxx is your initials
-
Repeatedly try the app at the new route. You should see traffic bouncing back and forth between the two versions of the app
-
Remove the route from version 1.0 of the app:
cf unmap-route NetLoanCalculator-1.0 apps.pcfpoc.jgbpcf.com --hostname xxx-loancalculator
Where xxx is your initials
-
You should now see traffic only going to the new version of the app
-
Delete version 1.0 of the app:
cf delete NetLoanCalculator-1.0 -r
This deletes the app and its route