├── LICENSE ├── README.md └── src ├── .gitignore ├── ContosoLending.CurrencyExchange ├── ContosoLending.CurrencyExchange.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── Protos │ └── exchange_rate.proto ├── Services │ └── ExchangeRateService.cs ├── Startup.cs ├── appsettings.Development.json └── appsettings.json ├── ContosoLending.DomainModel ├── Applicant.cs ├── Constants.cs ├── ContosoLending.DomainModel.csproj ├── CurrencyConversion.cs ├── LoanAmount.cs └── LoanApplication.cs ├── ContosoLending.LoanProcessing ├── ContosoLending.LoanProcessing.csproj ├── Functions │ ├── CheckCreditAgency.cs │ ├── HttpStart.cs │ ├── Negotiate.cs │ ├── Orchestrate.cs │ └── Receive.cs ├── Models │ ├── CreditAgencyRequest.cs │ ├── CreditAgencyResult.cs │ └── LoanApplicationResult.cs └── host.json ├── ContosoLending.Ui ├── App.razor ├── Components │ ├── CurrencyPicker.razor │ ├── Step.razor │ └── StepContent.razor ├── ContosoLending.Ui.csproj ├── Extensions │ └── ServiceCollectionExtensions.cs ├── Pages │ ├── Error.razor │ ├── LoanApplication.razor │ ├── LoanDashboard.razor │ └── _Host.cshtml ├── Program.cs ├── Properties │ └── launchSettings.json ├── Services │ ├── CurrencyConversionService.cs │ └── LendingService.cs ├── Shared │ ├── MainLayout.razor │ └── NavMenu.razor ├── Startup.cs ├── ViewModels │ ├── Applicant.cs │ ├── LoanAmount.cs │ └── LoanApplication.cs ├── _Imports.razor ├── appsettings.Development.json ├── appsettings.json ├── libman.json └── wwwroot │ ├── css │ └── site.css │ ├── favicon.ico │ └── js │ └── loan-dashboard.js └── ContosoLending.sln /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Scott Addie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## App overview 2 | 3 | This ASP.NET Core 3.1 app represents a loan application processing pipeline. The following table outlines projects found in the solution. 4 | 5 | |Project |Description | 6 | |---------------------------------|------------------------------------------------------| 7 | |*ContosoLending.CurrencyExchange*|gRPC project handling currency conversion | 8 | |*ContosoLending.DomainModel* |.NET Standard project containing shared models | 9 | |*ContosoLending.LoanProcessing* |Durable Functions project for handling loan processing| 10 | |*ContosoLending.Ui* |Server-side Blazor UI project | 11 | 12 | ## Setup 13 | 14 | ### Install prerequisites 15 | 16 | The following software must be installed: 17 | 18 | 1. [.NET Core SDK version SDK 3.1.100 or later](https://dotnet.microsoft.com/download/dotnet-core/3.1) 19 | 1. [Visual Studio 2019 version 16.4 or later](https://visualstudio.microsoft.com/downloads/) with the following workloads: 20 | 1. **ASP.NET and web development** 21 | 1. **Azure development** 22 | 23 | ### Provision Azure resources 24 | 25 | 1. Open the [Azure Cloud Shell](https://shell.azure.com) in your web browser. 26 | 27 | 1. Run the following command to configure your Azure CLI defaults for resource group and region: 28 | 29 | ```bash 30 | az configure --defaults group= location= 31 | ``` 32 | 33 | 1. Run the following command to provision an Azure Storage account: 34 | 35 | ```bash 36 | az storage account create --name 37 | ``` 38 | 39 | 1. Run the following command to provision an Azure SignalR Service instance: 40 | 41 | ```bash 42 | az signalr create --name --sku Standard_S1 --service-mode Serverless 43 | ``` 44 | 45 | ### Configure the Azure Functions project 46 | 47 | 1. Create a new *local.settings.json* file in the root of the *ContosoLending.LoanProcessing* project with the following content: 48 | 49 | ```json 50 | { 51 | "IsEncrypted": false, 52 | "Values": { 53 | "AzureSignalRConnectionString": "", 54 | "AzureWebJobsStorage": "", 55 | "FUNCTIONS_WORKER_RUNTIME": "dotnet" 56 | }, 57 | "Host": { 58 | "CORS": "https://localhost:44364", 59 | "CORSCredentials": true, 60 | "LocalHttpPort": 7071 61 | } 62 | } 63 | ``` 64 | 65 | 1. From the Azure Cloud Shell, run the following command to get the Azure Storage account's connection string: 66 | 67 | ```bash 68 | az storage account show-connection-string --name --query connectionString 69 | ``` 70 | 71 | Copy the resulting value (without the double quotes) to your clipboard. 72 | 73 | 1. Replace "<storage_connection_string>" in *local.settings.json* with the value on your clipboard. 74 | 75 | 1. Run the following command to get the Azure SignalR Service's connection string: 76 | 77 | ```bash 78 | az signalr key list --name --query primaryConnectionString 79 | ``` 80 | 81 | Copy the resulting value (without the double quotes) to your clipboard. 82 | 83 | 1. Replace "<signalr_connection_string>" in *local.settings.json* with the value on your clipboard. 84 | 85 | ## Testing 86 | 87 | 1. Open the solution file (*src\ContosoLending.sln*). 88 | 1. In **Solution Explorer**, right-click the *libman.json* file in the **ContosoLending.Ui** project > **Restore Client-Side Libraries**. 89 | 1. In **Solution Explorer**, right-click the solution name > **Properties**. 90 | 1. Select the **Multiple startup projects** radio button, and configure the solution as follows: 91 | 92 | ![multiple project launch configuration in Visual Studio](https://user-images.githubusercontent.com/10702007/68152936-39716780-ff0a-11e9-9f62-babf2267ef77.png) 93 | 94 | 1. Select the **OK** button. 95 | 1. Select the **Start** button next to the **<Multiple Startup Projects>** launch configuration drop-down list. 96 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | **/wwwroot/lib/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /src/ContosoLending.CurrencyExchange/ContosoLending.CurrencyExchange.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/ContosoLending.CurrencyExchange/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Hosting; 4 | 5 | namespace ContosoLending.CurrencyExchange 6 | { 7 | public class Program 8 | { 9 | public static Task Main(string[] args) => 10 | CreateHostBuilder(args).Build().RunAsync(); 11 | 12 | // Additional configuration is required to successfully run gRPC on macOS. 13 | // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 14 | private static IHostBuilder CreateHostBuilder(string[] args) => 15 | Host.CreateDefaultBuilder(args) 16 | .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ContosoLending.CurrencyExchange/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "ContosoLending.CurrencyExchange": { 4 | "commandName": "Project", 5 | "launchBrowser": false, 6 | "applicationUrl": "https://localhost:5002", 7 | "environmentVariables": { 8 | "ASPNETCORE_ENVIRONMENT": "Development" 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/ContosoLending.CurrencyExchange/Protos/exchange_rate.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option csharp_namespace = "ContosoLending.CurrencyExchange"; 4 | package contosolending.currencyexchange; 5 | 6 | service ExchangeRateManager { 7 | rpc GetExchangeRate (ExchangeRateRequest) returns (ExchangeRateReply); 8 | } 9 | 10 | message ExchangeRateRequest { 11 | string currency_type_from = 1; 12 | string currency_type_to = 2; 13 | } 14 | 15 | message ExchangeRateReply { 16 | double exchange_rate = 1; 17 | } 18 | -------------------------------------------------------------------------------- /src/ContosoLending.CurrencyExchange/Services/ExchangeRateService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Grpc.Core; 3 | using Microsoft.Extensions.Logging; 4 | using static ContosoLending.DomainModel.Constants; 5 | 6 | namespace ContosoLending.CurrencyExchange.Services 7 | { 8 | public class ExchangeRateService : ExchangeRateManager.ExchangeRateManagerBase 9 | { 10 | private readonly ILogger _logger; 11 | 12 | public ExchangeRateService(ILogger logger) 13 | { 14 | _logger = logger; 15 | } 16 | 17 | public override Task GetExchangeRate(ExchangeRateRequest request, ServerCallContext context) 18 | { 19 | double exchangeRate = 0.00; 20 | 21 | if ((request.CurrencyTypeFrom == UsDollarAlias && request.CurrencyTypeTo == BulgarianLevAlias) || 22 | (request.CurrencyTypeFrom == BulgarianLevAlias && request.CurrencyTypeTo == UsDollarAlias)) 23 | { 24 | exchangeRate = 0.56; 25 | } 26 | 27 | var reply = new ExchangeRateReply 28 | { 29 | ExchangeRate = exchangeRate 30 | }; 31 | 32 | return Task.FromResult(reply); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/ContosoLending.CurrencyExchange/Startup.cs: -------------------------------------------------------------------------------- 1 | using ContosoLending.CurrencyExchange.Services; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | 8 | namespace ContosoLending.CurrencyExchange 9 | { 10 | public class Startup 11 | { 12 | public void ConfigureServices(IServiceCollection services) 13 | { 14 | services.AddGrpc(); 15 | } 16 | 17 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 18 | { 19 | if (env.IsDevelopment()) 20 | { 21 | app.UseDeveloperExceptionPage(); 22 | } 23 | 24 | app.UseRouting(); 25 | 26 | app.UseEndpoints(endpoints => 27 | { 28 | endpoints.MapGrpcService(); 29 | 30 | endpoints.MapGet("/proto", async req => 31 | await req.Response.SendFileAsync("Protos/exchange_rate.proto", req.RequestAborted)); 32 | 33 | endpoints.MapGet("/", async req => 34 | await req.Response.WriteAsync("Healthy")); 35 | }); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/ContosoLending.CurrencyExchange/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Grpc": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/ContosoLending.CurrencyExchange/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning", 5 | "Microsoft.Hosting.Lifetime": "Information" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "Kestrel": { 10 | "EndpointDefaults": { 11 | "Protocols": "Http2" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/ContosoLending.DomainModel/Applicant.cs: -------------------------------------------------------------------------------- 1 | namespace ContosoLending.DomainModel 2 | { 3 | public class Applicant 4 | { 5 | public string FirstName { get; set; } 6 | 7 | public string LastName { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/ContosoLending.DomainModel/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace ContosoLending.DomainModel 2 | { 3 | public static class Constants 4 | { 5 | public const string BulgarianLevAlias = "Lev"; 6 | public const string BulgarianLevSymbol = "Лв."; 7 | 8 | public const string UsDollarAlias = "USD"; 9 | public const string UsDollarSymbol = "$"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/ContosoLending.DomainModel/ContosoLending.DomainModel.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/ContosoLending.DomainModel/CurrencyConversion.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using static ContosoLending.DomainModel.Constants; 3 | 4 | namespace ContosoLending.DomainModel 5 | { 6 | public class CurrencyConversion 7 | { 8 | public Currency CurrencyTypeFrom { get; set; } 9 | 10 | public Currency CurrencyTypeTo { get; set; } 11 | 12 | public decimal AmountToConvert { get; set; } 13 | } 14 | 15 | public enum Currency 16 | { 17 | USDollar = 0, 18 | BulgarianLev = 1, 19 | } 20 | 21 | public static class CurrencyExtensions 22 | { 23 | public static string ToAlias(this Currency currency) => 24 | currency switch 25 | { 26 | Currency.BulgarianLev => BulgarianLevAlias, 27 | Currency.USDollar => UsDollarAlias, 28 | _ => throw new ArgumentException(message: "invalid enum value", paramName: nameof(currency)), 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/ContosoLending.DomainModel/LoanAmount.cs: -------------------------------------------------------------------------------- 1 | namespace ContosoLending.DomainModel 2 | { 3 | public class LoanAmount 4 | { 5 | public decimal Amount { get; set; } 6 | 7 | public string CurrencyType { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/ContosoLending.DomainModel/LoanApplication.cs: -------------------------------------------------------------------------------- 1 | namespace ContosoLending.DomainModel 2 | { 3 | public class LoanApplication 4 | { 5 | public Applicant Applicant { get; set; } = new Applicant(); 6 | 7 | public LoanAmount LoanAmount { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/ContosoLending.LoanProcessing/ContosoLending.LoanProcessing.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp3.1 4 | v3 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | PreserveNewest 17 | 18 | 19 | PreserveNewest 20 | Never 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/ContosoLending.LoanProcessing/Functions/CheckCreditAgency.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using ContosoLending.LoanProcessing.Models; 4 | using Microsoft.Azure.WebJobs; 5 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 6 | using Microsoft.Azure.WebJobs.Extensions.SignalRService; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace ContosoLending.LoanProcessing.Functions 10 | { 11 | public static partial class Functions 12 | { 13 | [FunctionName(nameof(CheckCreditAgency))] 14 | public async static Task CheckCreditAgency( 15 | [ActivityTrigger] CreditAgencyRequest request, 16 | [SignalR(HubName = "dashboard")] IAsyncCollector dashboardMessages, 17 | ILogger log) 18 | { 19 | log.LogWarning($"Checking agency {request.AgencyName} for customer {request.Application.Applicant.ToString()} for {request.Application.LoanAmount}"); 20 | 21 | await dashboardMessages.AddAsync(new SignalRMessage 22 | { 23 | Target = "agencyCheckStarted", 24 | Arguments = new object[] { request } 25 | }); 26 | 27 | var rnd = new Random(); 28 | await Task.Delay(rnd.Next(2000, 4000)); // simulate variant processing times 29 | 30 | var result = new CreditAgencyResult 31 | { 32 | IsApproved = !(request.AgencyName.Contains("Woodgrove") && request.Application.LoanAmount.Amount > 4999), 33 | Application = request.Application, 34 | AgencyId = request.AgencyId 35 | }; 36 | 37 | await dashboardMessages.AddAsync(new SignalRMessage 38 | { 39 | Target = "agencyCheckComplete", 40 | Arguments = new object[] { result } 41 | }); 42 | 43 | log.LogWarning($"Agency {request.AgencyName} {(result.IsApproved ? "APPROVED" : "DECLINED")} request by customer {request.Application.Applicant.ToString()} for {request.Application.LoanAmount}"); 44 | 45 | return result; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ContosoLending.LoanProcessing/Functions/HttpStart.cs: -------------------------------------------------------------------------------- 1 | using ContosoLending.DomainModel; 2 | using Microsoft.Azure.WebJobs; 3 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 4 | using Microsoft.Azure.WebJobs.Extensions.Http; 5 | using Microsoft.Extensions.Logging; 6 | using System.Net.Http; 7 | using System.Text.Json; 8 | using System.Threading.Tasks; 9 | 10 | namespace ContosoLending.LoanProcessing.Functions 11 | { 12 | public static partial class Functions 13 | { 14 | [FunctionName(nameof(HttpStart))] 15 | public static async Task HttpStart( 16 | [HttpTrigger(AuthorizationLevel.Anonymous, "post")]HttpRequestMessage req, 17 | [DurableClient]IDurableOrchestrationClient starter, 18 | ILogger log) 19 | { 20 | using var jsonStream = await req.Content.ReadAsStreamAsync(); 21 | var loanApplication = await JsonSerializer.DeserializeAsync(jsonStream); 22 | 23 | string instanceId = await starter.StartNewAsync(nameof(Orchestrate), loanApplication); 24 | 25 | log.LogInformation($"Started orchestration with ID = '{instanceId}'."); 26 | 27 | return starter.CreateCheckStatusResponse(req, instanceId); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/ContosoLending.LoanProcessing/Functions/Negotiate.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.Azure.WebJobs; 3 | using Microsoft.Azure.WebJobs.Extensions.Http; 4 | using Microsoft.Azure.WebJobs.Extensions.SignalRService; 5 | 6 | namespace ContosoLending.LoanProcessing.Functions 7 | { 8 | public static partial class Functions 9 | { 10 | [FunctionName(nameof(Negotiate))] 11 | public static SignalRConnectionInfo Negotiate( 12 | [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequest req, 13 | [SignalRConnectionInfo(HubName = "dashboard")] SignalRConnectionInfo connectionInfo) => connectionInfo; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/ContosoLending.LoanProcessing/Functions/Orchestrate.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using ContosoLending.DomainModel; 5 | using ContosoLending.LoanProcessing.Models; 6 | using Microsoft.Azure.WebJobs; 7 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 8 | using Microsoft.Azure.WebJobs.Extensions.SignalRService; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace ContosoLending.LoanProcessing.Functions 12 | { 13 | public static partial class Functions 14 | { 15 | [FunctionName(nameof(Orchestrate))] 16 | public static async Task Orchestrate( 17 | [OrchestrationTrigger] IDurableOrchestrationContext context, 18 | [SignalR(HubName = "dashboard")] IAsyncCollector dashboardMessages, 19 | ILogger logger) 20 | { 21 | var loanApplication = context.GetInput(); 22 | var agencyTasks = new List>(); 23 | var agencies = new List(); 24 | var results = new CreditAgencyResult[] { }; 25 | 26 | logger.LogWarning($"Status of application for {loanApplication.Applicant.ToString()} for {loanApplication.LoanAmount}: Checking with agencies."); 27 | 28 | // start the process and perform initial validation 29 | bool loanStarted = await context.CallActivityAsync(nameof(Receive), loanApplication); 30 | 31 | // fan out and check the credit agencies 32 | if (loanStarted) 33 | { 34 | agencies.AddRange(new CreditAgencyRequest[] { 35 | new CreditAgencyRequest { AgencyName = "Contoso, Ltd.", AgencyId = "contoso", Application = loanApplication }, 36 | new CreditAgencyRequest { AgencyName = "Fabrikam, Inc.", AgencyId = "fabrikam", Application = loanApplication }, 37 | new CreditAgencyRequest { AgencyName = "Woodgrove Bank", AgencyId = "woodgrove", Application = loanApplication }, 38 | }); 39 | 40 | foreach (var agency in agencies) 41 | { 42 | agencyTasks.Add(context.CallActivityAsync(nameof(CheckCreditAgency), agency)); 43 | } 44 | 45 | await dashboardMessages.AddAsync(new SignalRMessage 46 | { 47 | Target = "agencyCheckPhaseStarted", 48 | Arguments = new object[] { } 49 | }); 50 | 51 | // wait for all the agencies to return their results 52 | results = await Task.WhenAll(agencyTasks); 53 | 54 | await dashboardMessages.AddAsync(new SignalRMessage 55 | { 56 | Target = "agencyCheckPhaseCompleted", 57 | Arguments = new object[] { !(results.Any(x => x.IsApproved == false)) } 58 | }); 59 | } 60 | 61 | var response = new LoanApplicationResult 62 | { 63 | Application = loanApplication, 64 | IsApproved = loanStarted && !(results.Any(x => x.IsApproved == false)) 65 | }; 66 | 67 | logger.LogWarning($"Agency checks result with {response.IsApproved} for loan amount of {response.Application.LoanAmount} to customer {response.Application.Applicant.ToString()}"); 68 | 69 | await dashboardMessages.AddAsync(new SignalRMessage 70 | { 71 | Target = "loanApplicationComplete", 72 | Arguments = new object[] { response } 73 | }); 74 | 75 | return response; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/ContosoLending.LoanProcessing/Functions/Receive.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using ContosoLending.DomainModel; 4 | using Microsoft.Azure.WebJobs; 5 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 6 | using Microsoft.Azure.WebJobs.Extensions.SignalRService; 7 | 8 | namespace ContosoLending.LoanProcessing.Functions 9 | { 10 | public static partial class Functions 11 | { 12 | [FunctionName(nameof(Receive))] 13 | public async static Task Receive( 14 | [SignalR(HubName = "dashboard")] IAsyncCollector dashboardMessages, 15 | [ActivityTrigger] LoanApplication loanApplication) 16 | { 17 | await dashboardMessages.AddAsync(new SignalRMessage 18 | { 19 | Target = "loanApplicationStart", 20 | Arguments = new object[] { loanApplication } 21 | }); 22 | 23 | await Task.Delay(new Random().Next(1000, 3000)); // simulate variant processing times 24 | 25 | bool result = loanApplication.LoanAmount.Amount < 10000; 26 | 27 | await dashboardMessages.AddAsync(new SignalRMessage 28 | { 29 | Target = "loanApplicationReceived", 30 | Arguments = new object[] { loanApplication, result } 31 | }); 32 | 33 | return result; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ContosoLending.LoanProcessing/Models/CreditAgencyRequest.cs: -------------------------------------------------------------------------------- 1 | using ContosoLending.DomainModel; 2 | 3 | namespace ContosoLending.LoanProcessing.Models 4 | { 5 | public class CreditAgencyRequest 6 | { 7 | public string AgencyId { get; set; } 8 | public string AgencyName { get; set; } 9 | public LoanApplication Application { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/ContosoLending.LoanProcessing/Models/CreditAgencyResult.cs: -------------------------------------------------------------------------------- 1 | using ContosoLending.DomainModel; 2 | 3 | namespace ContosoLending.LoanProcessing.Models 4 | { 5 | public class CreditAgencyResult 6 | { 7 | public string AgencyId { get; set; } 8 | public LoanApplication Application { get; set; } 9 | public bool IsApproved { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/ContosoLending.LoanProcessing/Models/LoanApplicationResult.cs: -------------------------------------------------------------------------------- 1 | using ContosoLending.DomainModel; 2 | 3 | namespace ContosoLending.LoanProcessing.Models 4 | { 5 | public class LoanApplicationResult 6 | { 7 | public LoanApplication Application { get; set; } 8 | public bool IsApproved { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/ContosoLending.LoanProcessing/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0" 3 | } -------------------------------------------------------------------------------- /src/ContosoLending.Ui/App.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 |

Sorry, there's nothing at this address.

8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /src/ContosoLending.Ui/Components/CurrencyPicker.razor: -------------------------------------------------------------------------------- 1 | @using ContosoLending.DomainModel 2 | @using ContosoLending.Ui.Services 3 | @using static ContosoLending.DomainModel.Constants 4 | @inject CurrencyConversionService CurrencyConversionService 5 | 6 |
7 | 8 |
9 | 10 | 11 | @code { 12 | private bool IsTextBoxDisabled { get; set; } 13 | 14 | [Parameter] 15 | public ViewModels.LoanAmount LoanAmount { get; set; } 16 | 17 | [Parameter] 18 | public EventCallback LoanAmountChanged { get; set; } 19 | 20 | private async Task SwitchCurrencyType() 21 | { 22 | IsTextBoxDisabled = true; 23 | 24 | Currency currencyTypeFrom = 25 | CurrencyConversionService.GetCurrencyEnumValueFromSymbol(LoanAmount.CurrencyType); 26 | LoanAmount.CurrencyType = LoanAmount.CurrencyType == UsDollarSymbol 27 | ? BulgarianLevSymbol : UsDollarSymbol; 28 | 29 | if (LoanAmount.Amount > 0.00m) 30 | { 31 | Currency currencyTypeTo = 32 | CurrencyConversionService.GetCurrencyEnumValueFromSymbol(LoanAmount.CurrencyType); 33 | 34 | var conversion = new CurrencyConversion 35 | { 36 | CurrencyTypeFrom = currencyTypeFrom, 37 | CurrencyTypeTo = currencyTypeTo, 38 | AmountToConvert = LoanAmount.Amount, 39 | }; 40 | 41 | LoanAmount.Amount = await CurrencyConversionService.GetConvertedAmountAsync(conversion); 42 | } 43 | 44 | IsTextBoxDisabled = false; 45 | await LoanAmountChanged.InvokeAsync(LoanAmount); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/ContosoLending.Ui/Components/Step.razor: -------------------------------------------------------------------------------- 1 | 
2 | @ChildContent 3 |
4 | 5 | @code { 6 | [Parameter] 7 | public string Id { get; set; } 8 | 9 | [Parameter] 10 | public RenderFragment ChildContent { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /src/ContosoLending.Ui/Components/StepContent.razor: -------------------------------------------------------------------------------- 1 | 
2 |
@Title
3 |
@Description
4 |
5 | 6 | @code { 7 | [Parameter] 8 | public string Title { get; set; } 9 | 10 | [Parameter] 11 | public string Description { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /src/ContosoLending.Ui/ContosoLending.Ui.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/ContosoLending.Ui/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace ContosoLending.Ui.Extensions 5 | { 6 | public static class ServiceCollectionExtensions 7 | { 8 | public static void AddAutoMapper(this IServiceCollection services) 9 | { 10 | var mappingConfig = new MapperConfiguration(config => 11 | config.AddProfile(new MappingProfile())); 12 | 13 | IMapper mapper = mappingConfig.CreateMapper(); 14 | services.AddSingleton(mapper); 15 | } 16 | } 17 | 18 | public class MappingProfile : Profile 19 | { 20 | public MappingProfile() 21 | { 22 | CreateMap(); 23 | CreateMap(); 24 | CreateMap(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ContosoLending.Ui/Pages/Error.razor: -------------------------------------------------------------------------------- 1 | @page "/error" 2 | 3 | 4 |

Error.

5 |

An error occurred while processing your request.

6 | 7 |

Development Mode

8 |

9 | Swapping to Development environment will display more detailed information about the error that occurred. 10 |

11 |

12 | The Development environment shouldn't be enabled for deployed applications. 13 | It can result in displaying sensitive information from exceptions to end users. 14 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 15 | and restarting the app. 16 |

-------------------------------------------------------------------------------- /src/ContosoLending.Ui/Pages/LoanApplication.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @using ContosoLending.Ui.Components 3 | @inject NavigationManager NavigationManager 4 | @inject Services.LendingService LendingService 5 | 6 |

Loan Application

7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 |
22 |
23 | 24 |
25 | 26 |
27 | 28 |
29 | 30 | 31 |
32 | 33 | @code { 34 | private ViewModels.LoanApplication LoanApp = new ViewModels.LoanApplication(); 35 | 36 | private bool IsDisabled { get; set; } 37 | 38 | private async Task SubmitLoanApp() 39 | { 40 | IsDisabled = true; 41 | await LendingService.SubmitLoanAppAsync(LoanApp); 42 | NavigationManager.NavigateTo("/loandashboard"); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/ContosoLending.Ui/Pages/LoanDashboard.razor: -------------------------------------------------------------------------------- 1 | @page "/loandashboard" 2 | @using ContosoLending.Ui.Components 3 | @using Microsoft.Extensions.Configuration 4 | @inject IJSRuntime JsRuntime 5 | @inject IConfiguration Configuration 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 |
23 |
24 |
25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 |
42 |
43 |
44 |
45 |
46 | 47 | @code { 48 | protected override async Task OnInitializedAsync() => 49 | await JsRuntime.InvokeVoidAsync("start", Configuration["LendingService:BaseAddress"]); 50 | } 51 | -------------------------------------------------------------------------------- /src/ContosoLending.Ui/Pages/_Host.cshtml: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @namespace ContosoLending.Ui.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | 5 | 6 | 7 | 8 | 9 | 10 | Contoso Lending 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | An error has occurred. This application may no longer respond until reloaded. 22 | 23 | 24 | An unhandled exception has occurred. See browser dev tools for details. 25 | 26 | Reload 27 | 🗙 28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/ContosoLending.Ui/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | using System.Threading.Tasks; 4 | 5 | namespace ContosoLending.Ui 6 | { 7 | public class Program 8 | { 9 | public static Task Main(string[] args) => 10 | CreateHostBuilder(args).Build().RunAsync(); 11 | 12 | private static IHostBuilder CreateHostBuilder(string[] args) => 13 | Host.CreateDefaultBuilder(args) 14 | .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/ContosoLending.Ui/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:51704", 7 | "sslPort": 44364 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development", 16 | "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" 17 | } 18 | }, 19 | "ContosoLending.Ui": { 20 | "commandName": "Project", 21 | "launchBrowser": true, 22 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development", 25 | "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ContosoLending.Ui/Services/CurrencyConversionService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using ContosoLending.CurrencyExchange; 4 | using ContosoLending.DomainModel; 5 | using Grpc.Net.Client; 6 | using Microsoft.Extensions.Configuration; 7 | using static ContosoLending.DomainModel.Constants; 8 | 9 | namespace ContosoLending.Ui.Services 10 | { 11 | public class CurrencyConversionService 12 | { 13 | private readonly IConfiguration _configuration; 14 | 15 | public CurrencyConversionService(IConfiguration configuration) 16 | { 17 | _configuration = configuration; 18 | } 19 | 20 | private async Task GetExchangeRateAsync(Currency currencyTypeFrom, Currency currencyTypeTo) 21 | { 22 | using var channel = GrpcChannel.ForAddress(_configuration["ExchangeRateService:BaseAddress"]); 23 | var client = new ExchangeRateManager.ExchangeRateManagerClient(channel); 24 | var request = new ExchangeRateRequest 25 | { 26 | CurrencyTypeFrom = currencyTypeFrom.ToAlias(), 27 | CurrencyTypeTo = currencyTypeTo.ToAlias(), 28 | }; 29 | 30 | ExchangeRateReply exchangeRate = await client.GetExchangeRateAsync(request); 31 | 32 | return exchangeRate.ExchangeRate; 33 | } 34 | 35 | public async Task GetConvertedAmountAsync(CurrencyConversion conversion) 36 | { 37 | decimal exchangeRate = Convert.ToDecimal(await GetExchangeRateAsync( 38 | conversion.CurrencyTypeFrom, conversion.CurrencyTypeTo)); 39 | 40 | decimal convertedAmount = conversion.CurrencyTypeTo switch 41 | { 42 | Currency.BulgarianLev => decimal.Round(conversion.AmountToConvert / exchangeRate, 2), 43 | _ => decimal.Round(conversion.AmountToConvert * exchangeRate, 2), 44 | }; 45 | 46 | return convertedAmount; 47 | } 48 | 49 | public Currency GetCurrencyEnumValueFromSymbol(string currencyType) => 50 | currencyType switch 51 | { 52 | BulgarianLevSymbol => Currency.BulgarianLev, 53 | UsDollarSymbol => Currency.USDollar, 54 | _ => throw new ArgumentException(message: "invalid currency type", paramName: nameof(currencyType)), 55 | }; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/ContosoLending.Ui/Services/LendingService.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading.Tasks; 3 | using AutoMapper; 4 | using DM = ContosoLending.DomainModel; 5 | 6 | namespace ContosoLending.Ui.Services 7 | { 8 | public class LendingService 9 | { 10 | private readonly HttpClient _httpClient; 11 | private readonly IMapper _mapper; 12 | 13 | public LendingService(HttpClient httpClient, IMapper mapper) 14 | { 15 | _httpClient = httpClient; 16 | _mapper = mapper; 17 | } 18 | 19 | public async Task SubmitLoanAppAsync(ViewModels.LoanApplication loanApp) 20 | { 21 | var model = _mapper.Map(loanApp); 22 | await _httpClient.PostAsJsonAsync(_httpClient.BaseAddress.AbsoluteUri, model); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ContosoLending.Ui/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 | 6 | 7 |
8 |
9 | GitHub repo 10 |
11 | 12 |
13 | @Body 14 |
15 |
16 | -------------------------------------------------------------------------------- /src/ContosoLending.Ui/Shared/NavMenu.razor: -------------------------------------------------------------------------------- 1 |  7 | 8 |
9 | 21 |
22 | 23 | @code { 24 | private bool collapseNavMenu = true; 25 | 26 | private string NavMenuCssClass => collapseNavMenu ? "collapse" : null; 27 | 28 | private void ToggleNavMenu() 29 | { 30 | collapseNavMenu = !collapseNavMenu; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ContosoLending.Ui/Startup.cs: -------------------------------------------------------------------------------- 1 | using ContosoLending.Ui.Extensions; 2 | using ContosoLending.Ui.Services; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Hosting; 8 | using System; 9 | 10 | namespace ContosoLending.Ui 11 | { 12 | public class Startup 13 | { 14 | public Startup(IConfiguration configuration) 15 | { 16 | Configuration = configuration; 17 | } 18 | 19 | public IConfiguration Configuration { get; } 20 | 21 | public void ConfigureServices(IServiceCollection services) 22 | { 23 | services.AddRazorPages(); 24 | services.AddServerSideBlazor(); 25 | services.AddGrpc(); 26 | services.AddSingleton(); 27 | services.AddAutoMapper(); 28 | 29 | IConfigurationSection loanServiceConfig = Configuration.GetSection("LendingService"); 30 | 31 | services.AddHttpClient(config => 32 | config.BaseAddress = new Uri( 33 | $"{loanServiceConfig["BaseAddress"]}{loanServiceConfig["Routes:HttpStart"]}")); 34 | } 35 | 36 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 37 | { 38 | if (env.IsDevelopment()) 39 | { 40 | app.UseDeveloperExceptionPage(); 41 | } 42 | else 43 | { 44 | app.UseExceptionHandler("/Error"); 45 | app.UseHsts(); 46 | } 47 | 48 | app.UseHttpsRedirection(); 49 | app.UseStaticFiles(); 50 | app.UseRouting(); 51 | 52 | app.UseEndpoints(endpoints => 53 | { 54 | endpoints.MapBlazorHub(); 55 | endpoints.MapFallbackToPage("/_Host"); 56 | }); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/ContosoLending.Ui/ViewModels/Applicant.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace ContosoLending.Ui.ViewModels 4 | { 5 | public class Applicant 6 | { 7 | [Required] 8 | public string FirstName { get; set; } 9 | 10 | [Required] 11 | public string LastName { get; set; } 12 | 13 | public override string ToString() => 14 | $"{LastName}, {FirstName}"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/ContosoLending.Ui/ViewModels/LoanAmount.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.ComponentModel.DataAnnotations; 4 | using static ContosoLending.DomainModel.Constants; 5 | 6 | namespace ContosoLending.Ui.ViewModels 7 | { 8 | public class LoanAmount 9 | { 10 | [DisplayName("Loan Amount")] 11 | [Range(1, 100000)] 12 | public decimal Amount { get; set; } = 0.00m; 13 | 14 | public string CurrencyType { get; set; } = UsDollarSymbol; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/ContosoLending.Ui/ViewModels/LoanApplication.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace ContosoLending.Ui.ViewModels 4 | { 5 | public class LoanApplication 6 | { 7 | [ValidateComplexType] 8 | public Applicant Applicant { get; set; } = new Applicant(); 9 | 10 | [ValidateComplexType] 11 | public LoanAmount LoanAmount { get; set; } = new LoanAmount(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/ContosoLending.Ui/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using Microsoft.AspNetCore.Authorization 3 | @using Microsoft.AspNetCore.Components.Authorization 4 | @using Microsoft.AspNetCore.Components.Forms 5 | @using Microsoft.AspNetCore.Components.Routing 6 | @using Microsoft.AspNetCore.Components.Web 7 | @using Microsoft.JSInterop 8 | @using ContosoLending.Ui 9 | @using ContosoLending.Ui.Shared 10 | -------------------------------------------------------------------------------- /src/ContosoLending.Ui/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/ContosoLending.Ui/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "ExchangeRateService": { 11 | "BaseAddress": "https://localhost:5002" 12 | }, 13 | "LendingService": { 14 | "BaseAddress": "http://localhost:7071/api", 15 | "Routes": { 16 | "HttpStart": "/HttpStart" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/ContosoLending.Ui/libman.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "libraries": [ 4 | { 5 | "provider": "jsdelivr", 6 | "library": "semantic-ui@2.4.2", 7 | "destination": "wwwroot/lib/semantic-ui/", 8 | "files": [ 9 | "dist/semantic.min.css", 10 | "dist/semantic.min.js", 11 | "dist/themes/default/assets/fonts/icons.eot", 12 | "dist/themes/default/assets/fonts/icons.ttf", 13 | "dist/themes/default/assets/fonts/icons.svg", 14 | "dist/themes/default/assets/fonts/icons.woff", 15 | "dist/themes/default/assets/fonts/icons.woff2" 16 | ] 17 | }, 18 | { 19 | "provider": "cdnjs", 20 | "library": "jquery@3.4.1", 21 | "destination": "wwwroot/lib/jquery/", 22 | "files": [ 23 | "jquery.min.js" 24 | ] 25 | }, 26 | { 27 | "provider": "cdnjs", 28 | "library": "twitter-bootstrap@4.3.1", 29 | "destination": "wwwroot/lib/twitter-bootstrap/", 30 | "files": [ 31 | "css/bootstrap.min.css" 32 | ] 33 | }, 34 | { 35 | "provider": "unpkg", 36 | "library": "@microsoft/signalr@3.1.3", 37 | "destination": "wwwroot/lib/@microsoft/signalr", 38 | "files": [ 39 | "dist/browser/signalr.min.js" 40 | ] 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /src/ContosoLending.Ui/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | } 4 | 5 | body { 6 | position: relative; 7 | display: flex; 8 | flex-direction: column; 9 | } 10 | 11 | a, .btn-link { 12 | color: #0366d6; 13 | } 14 | 15 | .btn-primary { 16 | color: #fff; 17 | background-color: #1b6ec2; 18 | border-color: #1861ac; 19 | } 20 | 21 | .top-row { 22 | height: 3.5rem; 23 | display: flex; 24 | align-items: center; 25 | } 26 | 27 | .main { 28 | flex: 1; 29 | } 30 | 31 | .main .top-row { 32 | background-color: #f7f7f7; 33 | border-bottom: 1px solid #d6d5d5; 34 | justify-content: flex-end; 35 | } 36 | 37 | .main .top-row > a { 38 | margin-left: 1.5rem; 39 | } 40 | 41 | .sidebar { 42 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 43 | } 44 | 45 | .sidebar .top-row { 46 | background-color: rgba(0,0,0,0.4); 47 | } 48 | 49 | .sidebar .navbar-brand { 50 | font-size: 1.1rem; 51 | } 52 | 53 | .sidebar .icon { 54 | width: 2rem; 55 | font-size: 1.1rem; 56 | top: -2px; 57 | } 58 | 59 | .nav-item { 60 | font-size: 0.9rem; 61 | padding-bottom: 0.5rem; 62 | } 63 | 64 | .nav-item:first-of-type { 65 | padding-top: 1rem; 66 | } 67 | 68 | .nav-item:last-of-type { 69 | padding-bottom: 1rem; 70 | } 71 | 72 | .nav-item a { 73 | color: #d7d7d7; 74 | border-radius: 4px; 75 | height: 3rem; 76 | display: flex; 77 | align-items: center; 78 | line-height: 3rem; 79 | } 80 | 81 | .nav-item a.active { 82 | background-color: rgba(255,255,255,0.25); 83 | color: white; 84 | } 85 | 86 | .nav-item a:hover { 87 | background-color: rgba(255,255,255,0.1); 88 | color: white; 89 | } 90 | 91 | .content { 92 | padding-top: 1.1rem; 93 | } 94 | 95 | .navbar-toggler { 96 | background-color: rgba(255, 255, 255, 0.1); 97 | } 98 | 99 | .valid.modified:not([type=checkbox]) { 100 | outline: 1px solid #26b050; 101 | } 102 | 103 | .invalid { 104 | outline: 1px solid red; 105 | } 106 | 107 | .validation-message { 108 | color: red; 109 | } 110 | 111 | #blazor-error-ui { 112 | background: lightyellow; 113 | bottom: 0; 114 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 115 | display: none; 116 | left: 0; 117 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 118 | position: fixed; 119 | width: 100%; 120 | z-index: 1000; 121 | } 122 | 123 | #blazor-error-ui .dismiss { 124 | cursor: pointer; 125 | position: absolute; 126 | right: 0.75rem; 127 | top: 0.5rem; 128 | } 129 | 130 | @media (max-width: 767.98px) { 131 | .main .top-row { 132 | display: none; 133 | } 134 | } 135 | 136 | @media (min-width: 768px) { 137 | body { 138 | flex-direction: row; 139 | } 140 | 141 | .sidebar { 142 | width: 250px; 143 | height: 100vh; 144 | position: sticky; 145 | top: 0; 146 | } 147 | 148 | .main .top-row { 149 | position: sticky; 150 | top: 0; 151 | } 152 | 153 | .main > div { 154 | padding-left: 2rem !important; 155 | padding-right: 1.5rem !important; 156 | } 157 | 158 | .navbar-toggler { 159 | display: none; 160 | } 161 | 162 | .sidebar .collapse { 163 | /* Never collapse the sidebar for wide screens */ 164 | display: block; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/ContosoLending.Ui/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottaddie/ContosoLending/082edc8d5ba8bc1db2788dbb71d9d47d6446a909/src/ContosoLending.Ui/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/ContosoLending.Ui/wwwroot/js/loan-dashboard.js: -------------------------------------------------------------------------------- 1 | function resetView() { 2 | ['confirmation', 'reception', 'agencies', 'contoso', 'fabrikam', 'woodgrove'] 3 | .forEach(element => setStepState(element)); 4 | } 5 | 6 | function setStepState(step, state) { 7 | const classesToRemove = ['disabled', 'active', 'completed']; 8 | document.getElementById(step).classList.remove(...classesToRemove); 9 | 10 | if (state) { 11 | document.getElementById(step).classList.add(state); 12 | 13 | if (state === 'disabled') { 14 | document.getElementById(step + '-icon').className = 'thumbs down icon'; 15 | } 16 | 17 | if (state === 'completed') { 18 | document.getElementById(step + '-icon').className = 'thumbs up icon'; 19 | } 20 | } 21 | else { // reset icon to original state 22 | const ogIcon = document.getElementById(step + '-icon').getAttribute('data-icon'); 23 | document.getElementById(step + '-icon').className = ogIcon + ' icon'; 24 | } 25 | } 26 | 27 | function registerEventHandlers(connection) { 28 | connection.on('loanApplicationStart', loanApplication => { 29 | resetView(); 30 | document.getElementById('reception').classList.add('active'); 31 | }); 32 | 33 | connection.on('loanApplicationReceived', (loanApplication, result) => { 34 | if (result === true) { 35 | setStepState('reception', 'completed'); 36 | } 37 | else { 38 | setStepState('reception', 'disabled'); 39 | } 40 | }); 41 | 42 | connection.on('agencyCheckPhaseStarted', () => { 43 | setStepState('agencies', 'active'); 44 | }); 45 | 46 | connection.on('agencyCheckPhaseCompleted', result => { 47 | if (result === true) { 48 | setStepState('agencies', 'completed'); 49 | } 50 | else { 51 | setStepState('agencies', 'disabled'); 52 | } 53 | }); 54 | 55 | connection.on('agencyCheckStarted', request => { 56 | setStepState(request.AgencyId, 'active'); 57 | }); 58 | 59 | connection.on('agencyCheckComplete', result => { 60 | if (result.IsApproved === true) { 61 | setStepState(result.AgencyId, 'completed'); 62 | } 63 | else { 64 | setStepState(result.AgencyId, 'disabled'); 65 | } 66 | }); 67 | 68 | connection.on('loanApplicationComplete', result => { 69 | if (result.IsApproved === true) { 70 | setStepState('confirmation', 'completed'); 71 | } 72 | else { 73 | setStepState('confirmation', 'disabled'); 74 | } 75 | }); 76 | 77 | connection.onreconnecting(error => { 78 | console.assert(connection.state === signalR.HubConnectionState.Reconnecting); 79 | console.error(error); 80 | }); 81 | 82 | connection.onreconnected(() => { 83 | console.assert(connection.state === signalR.HubConnectionState.Connected); 84 | }); 85 | } 86 | 87 | function buildHubConnection(hubUrl) { 88 | return new signalR.HubConnectionBuilder() 89 | .withUrl(hubUrl) 90 | .withAutomaticReconnect() 91 | .configureLogging(signalR.LogLevel.Information) 92 | .build(); 93 | } 94 | 95 | async function start(hubUrl) { 96 | const connection = buildHubConnection(hubUrl); 97 | 98 | try { 99 | registerEventHandlers(connection); 100 | await connection.start(); 101 | console.assert(connection.state === signalR.HubConnectionState.Connected); 102 | console.log('connected'); 103 | } catch (err) { 104 | console.assert(connection.state === signalR.HubConnectionState.Disconnected); 105 | console.error(err); 106 | setTimeout(() => start(), 5000); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/ContosoLending.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29311.281 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ContosoLending.Ui", "ContosoLending.Ui\ContosoLending.Ui.csproj", "{19F3634F-9844-4E17-8672-354A0B6E9862}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ContosoLending.LoanProcessing", "ContosoLending.LoanProcessing\ContosoLending.LoanProcessing.csproj", "{26BC329F-7671-4E01-869B-7093EFC91913}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F243328B-1CBF-4DB8-B20C-C6780159AA56}" 11 | ProjectSection(SolutionItems) = preProject 12 | .gitignore = .gitignore 13 | ..\README.md = ..\README.md 14 | EndProjectSection 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ContosoLending.DomainModel", "ContosoLending.DomainModel\ContosoLending.DomainModel.csproj", "{738A059D-B335-4E90-8AC6-AE5333936AE2}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ContosoLending.CurrencyExchange", "ContosoLending.CurrencyExchange\ContosoLending.CurrencyExchange.csproj", "{00B23998-EC03-465D-B313-D42B27EDCFB2}" 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Release|Any CPU = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {19F3634F-9844-4E17-8672-354A0B6E9862}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {19F3634F-9844-4E17-8672-354A0B6E9862}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {19F3634F-9844-4E17-8672-354A0B6E9862}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {19F3634F-9844-4E17-8672-354A0B6E9862}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {26BC329F-7671-4E01-869B-7093EFC91913}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {26BC329F-7671-4E01-869B-7093EFC91913}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {26BC329F-7671-4E01-869B-7093EFC91913}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {26BC329F-7671-4E01-869B-7093EFC91913}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {738A059D-B335-4E90-8AC6-AE5333936AE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {738A059D-B335-4E90-8AC6-AE5333936AE2}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {738A059D-B335-4E90-8AC6-AE5333936AE2}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {738A059D-B335-4E90-8AC6-AE5333936AE2}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {00B23998-EC03-465D-B313-D42B27EDCFB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {00B23998-EC03-465D-B313-D42B27EDCFB2}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {00B23998-EC03-465D-B313-D42B27EDCFB2}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {00B23998-EC03-465D-B313-D42B27EDCFB2}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {F45D7235-65A2-46AC-BE9B-FDDEE4EA3857} 48 | EndGlobalSection 49 | EndGlobal 50 | --------------------------------------------------------------------------------