diff --git a/.gitignore b/.gitignore index aa2fc52..9b9b2c9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,13 @@ goplugin/ bin/ .log -**/node_modules/ \ No newline at end of file +**/node_modules/ +NaveegoGrpcPlugin/NaveegoGrpcPlugin/obj/Debug/netcoreapp3.0/ + +NaveegoGrpcPlugin/.vs/NaveegoGrpcPlugin/v16/ + +NaveegoGrpcPlugin/NaveegoGrpcPlugin/obj/ + +*.txt + +NaveegoGrpcPlugin/.vs/NaveegoGrpcPlugin/DesignTimeBuild/.dtbcache diff --git a/Dockerfile b/Dockerfile index f3a8d89..d07be5e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,27 @@ +# I had to build my implementation first +FROM mcr.microsoft.com/dotnet/core/runtime:3.0 AS base +WORKDIR /app + +FROM mcr.microsoft.com/dotnet/core/sdk:3.0 AS build +WORKDIR /src +COPY NaveegoGrpcPlugin/NaveegoGrpcPlugin/NaveegoGrpcPlugin.csproj NaveegoGrpcPlugin/ +RUN dotnet restore NaveegoGrpcPlugin/NaveegoGrpcPlugin.csproj +COPY . . +WORKDIR /src/NaveegoGrpcPlugin/NaveegoGrpcPlugin +RUN dotnet build NaveegoGrpcPlugin.csproj -c Release -o /app + +FROM build AS publish +RUN dotnet publish NaveegoGrpcPlugin.csproj -c Release -o /app -r linux-x64 --self-contained true + +FROM base AS final +WORKDIR /app +COPY --from=publish /app . + FROM golang:1.11-stretch WORKDIR /code-challenge-plugin +#Then copy it into the go environment +COPY --from=final /app . ADD go.mod . ADD go.sum . @@ -14,9 +35,6 @@ ADD host.go . ENTRYPOINT ["go", "run", "host.go"] -# Build your implementation here - - +CMD ["./NaveegoGrpcPlugin"] -# Put your implementation here -CMD ["plugin"] +#ENTRYPOINT ["/bin/sh"] diff --git a/NaveegoGrpcPlugin/NaveegoGrpcPlugin.sln b/NaveegoGrpcPlugin/NaveegoGrpcPlugin.sln new file mode 100644 index 0000000..8089e20 --- /dev/null +++ b/NaveegoGrpcPlugin/NaveegoGrpcPlugin.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29318.209 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NaveegoGrpcPlugin", "NaveegoGrpcPlugin\NaveegoGrpcPlugin.csproj", "{65952571-BC78-418F-A4B6-1A46A1225AAB}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {65952571-BC78-418F-A4B6-1A46A1225AAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {65952571-BC78-418F-A4B6-1A46A1225AAB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {65952571-BC78-418F-A4B6-1A46A1225AAB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {65952571-BC78-418F-A4B6-1A46A1225AAB}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {4F447E73-BAC3-49DF-AFD2-4EF27FF8529D} + EndGlobalSection +EndGlobal diff --git a/NaveegoGrpcPlugin/NaveegoGrpcPlugin/Classes/Types.cs b/NaveegoGrpcPlugin/NaveegoGrpcPlugin/Classes/Types.cs new file mode 100644 index 0000000..6fc0fe7 --- /dev/null +++ b/NaveegoGrpcPlugin/NaveegoGrpcPlugin/Classes/Types.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace NaveegoGrpcPlugin +{ + public class Types + { + public string ColumnName { get; set; } + public Dictionary TypeVotes { get; private set; } + private bool typeFound; + + public Types(string columnName, string value) + { + ColumnName = columnName; + SetUpTypes(); + DetectTypes(value); + } + + private void SetUpTypes() + { + TypeVotes = new Dictionary(); + TypeVotes.Add(typeof(int), 0); + TypeVotes.Add(typeof(decimal), 0); + TypeVotes.Add(typeof(DateTime), 0); + TypeVotes.Add(typeof(bool), 0); + TypeVotes.Add(typeof(string), 0); + } + + + + public void DetectTypes(string value) + { + typeFound = false; + VoteForNumber(value); + VoteForInt(value); + VoteForDatetime(value); + VoteForBoolean(value); + if (!typeFound) + { + VoteForString(); + } + } + + + private void VoteForNumber(string value) + { + decimal decimalCheck; + decimal.TryParse(value, out decimalCheck); + if (decimalCheck != 0) + { + TypeVotes[typeof(decimal)] = TypeVotes[typeof(decimal)] + 1; + typeFound = true; + } + } + + private void VoteForInt(string value) + { + int integerCheck; + int.TryParse(value, out integerCheck); + if (integerCheck != 0) + { + TypeVotes[typeof(int)] = TypeVotes[typeof(int)] + 1; + typeFound = true; + } + } + + + private void VoteForDatetime(string value) + { + DateTime datetimeCheck; + DateTime.TryParse(value, out datetimeCheck); + if (datetimeCheck != DateTime.MinValue) + { + TypeVotes[typeof(DateTime)] = TypeVotes[typeof(DateTime)] + 1; + typeFound = true; + } + } + + private void VoteForBoolean(string value) + { + bool booleanCheck; + if (bool.TryParse(value, out booleanCheck)) + { + TypeVotes[typeof(bool)] = TypeVotes[typeof(bool)] + 1; + typeFound = true; + } + } + + private void VoteForString() + { + TypeVotes[typeof(string)] = TypeVotes[typeof(string)] + 1; + } + + public string TypeNameConvert(Type type) + { + if (type == typeof(string)) + return "string"; + else if (type == typeof(int)) + return "integer"; + else if (type == typeof(decimal)) + return "number"; + else if (type == typeof(DateTime)) + return "datetime"; + else if (type == typeof(bool)) + return "boolean"; + else + return string.Empty; + } + + + + } + + +} diff --git a/NaveegoGrpcPlugin/NaveegoGrpcPlugin/NaveegoGrpcPlugin.csproj b/NaveegoGrpcPlugin/NaveegoGrpcPlugin/NaveegoGrpcPlugin.csproj new file mode 100644 index 0000000..e00b9ad --- /dev/null +++ b/NaveegoGrpcPlugin/NaveegoGrpcPlugin/NaveegoGrpcPlugin.csproj @@ -0,0 +1,18 @@ + + + + netcoreapp3.0 + + + + + + + + + + + + + + diff --git a/NaveegoGrpcPlugin/NaveegoGrpcPlugin/Program.cs b/NaveegoGrpcPlugin/NaveegoGrpcPlugin/Program.cs new file mode 100644 index 0000000..8551477 --- /dev/null +++ b/NaveegoGrpcPlugin/NaveegoGrpcPlugin/Program.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Serilog; +using Serilog.Events; + +namespace NaveegoGrpcPlugin +{ + public class Program + { + + public static int Main(string[] args) + { + + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .MinimumLevel.Override("Microsoft", LogEventLevel.Information) + .Enrich.FromLogContext() + .WriteTo.File("PluginLog.txt", rollingInterval: RollingInterval.Day) + .CreateLogger(); + + try + { + AppDomain.CurrentDomain.ProcessExit += ProcessExitHandler; + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json") + .Build(); + CreateHostBuilder(args, configuration).Build().Run(); + return 0; + } + catch (Exception ex) + { + Log.Fatal(ex, "Host terminated unexpectedly"); + return 1; + } + finally + { + Log.CloseAndFlush(); + } + + } + + private static void ProcessExitHandler(object sender, EventArgs e) + { + Console.WriteLine("Shutting down"); + + } + + public static IHostBuilder CreateHostBuilder(string[] args, IConfiguration configuration) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.ConfigureKestrel(options => + { + var port = configuration.GetValue("GrpcPort"); + // Setup a HTTP/2 endpoint without TLS. + options.ListenLocalhost(port, o => o.Protocols = + HttpProtocols.Http2); + }); + webBuilder.UseStartup(); + webBuilder.UseSerilog(); + }); + } +} diff --git a/NaveegoGrpcPlugin/NaveegoGrpcPlugin/Properties/launchSettings.json b/NaveegoGrpcPlugin/NaveegoGrpcPlugin/Properties/launchSettings.json new file mode 100644 index 0000000..be249f5 --- /dev/null +++ b/NaveegoGrpcPlugin/NaveegoGrpcPlugin/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "NaveegoGrpcPlugin": { + "commandName": "Project", + "launchBrowser": false, + "applicationUrl": "https://localhost:5001", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/NaveegoGrpcPlugin/NaveegoGrpcPlugin/Protos/plugin.proto b/NaveegoGrpcPlugin/NaveegoGrpcPlugin/Protos/plugin.proto new file mode 100644 index 0000000..f38973b --- /dev/null +++ b/NaveegoGrpcPlugin/NaveegoGrpcPlugin/Protos/plugin.proto @@ -0,0 +1,134 @@ +syntax = "proto3"; +package plugin; + +// The Plugin service is implemented by plugins which are responsible for discovering +// and publishing data. In this challenge the plugin will target CSV files, but Naveego +// writes plugins against many different kinds of data sources. +// + +// One difficulty in creating a unified interface for interacting with multiple data sources +// is that the configuration settings needed to connect to data sources can differ radically. +// Naveego handles this by having each plugin define a JSONSchema for its settings, +// then dynamically rendering a form based on that schema which the user is asked to fill in. +// The resulting JSON object is then sent to the plugin for validation. In this challenge, +// we'll keep things simple and just hardcode the settings the plugin needs. +// +// Another complication is that data sources differ in the degree to which their schemas +// are discoverable. A SQL database usually provides metadata about the structure of tables, +// but a NoSQL database may not have any structure defined, and a collection of CSV files +// may provide a minimal structure by having headers, but with no type information. +// We handle that variation by providing a UI where a user can strongly specify the available +// schemas in collaboration with the plugin. The user provides some basic connection or +// selection information - such as a database name or a file path - and the plugin uses that +// to do its best to discover the schemas of the available data. We refer to this as the "discovery" +// phase. The user can then look at the discovered schemas, usually along with a sample of the +// data from the data source, and can assign types (like number, date, or boolean) to the +// properties of the schema, as well as annotating the properties with clear names and descriptions. +// The schemas authored by technical users are then made available to business users who can use +// them to construct their business-level data collection and merge flows. +// +// To help users, we'd like to be able to heuristically infer the types of properties that are +// discovered in data sources which do not provide strong type information. If you find +// implementing a passing solution not challenging enough, try adding some type inference logic +// which will examine the values in the data source in order to specify the types without user +// intervention. This may be tricky, since some records may have invalid data. If you've provided +// inferred types during discovery, you can mark records with bad data as invalid, but you +// should still publish the record. + +service Plugin { + // The Discover method is responsible for taking the provided settings + // and using them to find and describe all the schemas which the settings make available. + // In this case, the plugin will look for CSV files which match a pattern. + rpc Discover (DiscoverRequest) returns (DiscoverResponse) { + } + + // The Publish method is responsible for collecting all the records + // which belong to a single schema and streaming them back to the host. + // The schema which is passed in will be one of the schemas returned by the + // Discover method, so you can share data between Discover and Publish by + // means of the `settings` string on the Schema message. + rpc Publish (PublishRequest) returns (stream PublishRecord) { + } +} + +// The request message containing the user's name. +message DiscoverRequest { + // In a real plugin the settings would be conveyed in a JSON object + // with a schema defined by the plugin and populated by a user through a UI. + // For this challenge we'll define a message for the settings + // to make it easier for the host to create the settings. + Settings settings = 1; +} + +message Settings { + // This a glob specifying the pattern to use to find files. + // This will be an absolute path something like /src/data/*/*.csv. + // The plugin should find all files matching the pattern, then + // analyze them to find the unique schemas among them (multiple files + // may have the same schema). + // + // For this challenge you can assume that all CSV files have a header row, + // and that all files are comma delimited + string fileGlob = 1; +} + +message DiscoverResponse { + // Array of schemas discovered. + repeated Schema schemas = 1; +} + +message Schema { + // The unique name of the schema; if there is no unique name + // the plugin can generate one. + string name = 1; + // The settings the plugin will use for publishing when + // this schema is included in a PublishRequest. This + // can be any data the plugin wants to capture; the host + // will treat it as an opaque blob. + // Hint: this is a good place to store the file paths of all the + // files which contain records with this schema. + string settings = 2; + // Array of the properties discovered for this schema, + // in the order they have in the CSV. + repeated Property properties = 3; +} + +message Property { + // Name of the property, from the column header. + string name = 1; + // Type of the property; can be "string", "integer", "number", "datetime", "boolean" + // This should be inferred if possible by analyzing the data. + // This is an optional part of the challenge; you can pass the tests + // without populating this field. + string type = 2; +} + +message PublishRequest { + // The settings will be the same as the settings sent to the Discover method. + Settings settings = 1; + // The schema will be one of the schemas returned by the Discover method. + Schema schema = 2; +} + +message PublishRecord { + // This should be set to true if the record is not valid + // because it violates the inferred schema in some way. + // The invalid property should be written as `null` in data. + bool invalid = 1; + + // If the record is invalid this field should explain why. + // This should include the property name and the original value. + // This is a user-directed (as opposed to machine-readable) field, + // so you can format it however you want. + string error = 2; + // Data contains the values for a single record. + // The values should be provided as a JSON serialized array. + // For example, if the CSV has a row + // 17,Alabama,true + // then the data field should contain the string + // "[17,\"Alabama\",true]" + // however, if you have not inferred the types of the properties, + // it's OK to make everything a string, which would be + // "[\"17\",\"Alabama\",\"true\"]" + string data = 3; +} diff --git a/NaveegoGrpcPlugin/NaveegoGrpcPlugin/Services/PluginService.cs b/NaveegoGrpcPlugin/NaveegoGrpcPlugin/Services/PluginService.cs new file mode 100644 index 0000000..545a7ce --- /dev/null +++ b/NaveegoGrpcPlugin/NaveegoGrpcPlugin/Services/PluginService.cs @@ -0,0 +1,299 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Grpc.Core; +using Microsoft.Extensions.Logging; +using Plugin; +using Ganss.IO; +using System.IO; +using CsvHelper; +using Google.Protobuf.Collections; +using System.Text.Json; +using System.Globalization; + +namespace NaveegoGrpcPlugin +{ + public class PluginService : Plugin.Plugin.PluginBase + { + private readonly ILogger _logger; + private List discoveredSchemas; + private string errorMsg; + private readonly char delimiter; + public PluginService(ILogger logger) + { + _logger = logger; + delimiter = ';'; + discoveredSchemas = new List(); + } + + public override Task Discover(DiscoverRequest request, ServerCallContext context) + { + _logger.LogInformation("Received request to discover schemas."); + var examine = request.Settings.FileGlob; + LookForFiles(examine); + return Task.FromResult(new DiscoverResponse { Schemas = { discoveredSchemas } }); + + } + + public override async Task Publish(PublishRequest request, IServerStreamWriter responseStream, ServerCallContext context) + { + _logger.LogInformation("Received request to publish records."); + var filePaths = request.Schema.Settings.Split(delimiter); + var props = request.Schema.Properties; + foreach (var file in filePaths) + { + await GetDataToStream(file, props, responseStream); + } + } + + private void LookForFiles(string fileGlob) + { + _logger.LogInformation("Looking for files in {glob}", fileGlob); + try + { + + var foundFiles = Glob.Expand(fileGlob); + + if (!foundFiles.Any()) + { + _logger.LogInformation("Found no file matches"); + } + + foreach (var file in foundFiles) + { + _logger.LogInformation("Found {file}. Looking into it...", file.Name); + InvestigateFile(file.FullName); + } + + } + catch (Exception ex) + { + _logger.LogError(ex, "Error looking for files in glob"); + } + + } + + private void InvestigateFile(string filePath) + { + try + { + + var props = CreateProperties(filePath); + var foundSchema = CheckForExistingSchema(props); + if (foundSchema != null) + { + AppendFileToSchema(foundSchema, filePath); + } + else + { + discoveredSchemas.Add(CreateSchema(filePath, props)); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error investigating file."); + } + } + + private Schema CheckForExistingSchema(List props) + { + + foreach (var schema in discoveredSchemas) + { + var schemaArray = schema.Properties.ToArray(); + var propsArray = props.ToArray(); + if (schemaArray.Length == propsArray.Length && schemaArray.Intersect(propsArray).Count() == schemaArray.Length) + { + _logger.LogInformation("Found same schema called {name}. ", schema.Name); + return schema; + } + } + return null; + } + + private void AppendFileToSchema(Schema schema, string filePath) + { + schema.Settings += delimiter + filePath; + } + + private Schema CreateSchema(string fileName, List props) + { + string schemaName = "Schema" + (discoveredSchemas.Count + 1); + _logger.LogInformation("Creating schema called {name}. ", schemaName); + return new Schema { Name = schemaName, Settings = fileName, Properties = { props } }; + } + + private List CreateProperties(string filePath) + { + List scannedColumns = ScanForTypes(filePath); + + List newProps = new List(); + + foreach (var column in scannedColumns) + { + int max = 0; + string typeName = string.Empty; + foreach (var candidate in column.TypeVotes) + { + if (candidate.Value > max) + { + max = candidate.Value; + typeName = column.TypeNameConvert(candidate.Key); + } + } + newProps.Add(new Property { Name = column.ColumnName, Type = typeName }); + } + + return newProps; + } + + private List ScanForTypes(string filePath) + { + List types = new List(); + try + { + _logger.LogInformation("Scanning for types on {file}", filePath); + + using (var reader = new StreamReader(filePath)) + using (var csv = new CsvReader(reader)) + { + foreach (var record in csv.GetRecords()) + { + + foreach (KeyValuePair col in record) + { + var colName = col.Key.ToString(); + var field = col.Value.ToString(); + if (types.Where(w => w.ColumnName == colName).Any()) + { + var colType = types.Where(w => w.ColumnName == colName).First(); + colType.DetectTypes(field); + } + else + { + types.Add(new Types(colName, field)); + } + + } + + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while scanning CSVs."); + } + + return types; + } + + private async Task GetDataToStream(string filePath, RepeatedField props, IServerStreamWriter responseStream) + { + try + { + _logger.LogInformation("Publishing records for {file}", filePath); + using (var reader = new StreamReader(filePath)) + using (var csv = new CsvReader(reader)) + { + + foreach (var record in csv.GetRecords()) + { + var newPublishRecord = PrepareRecordForPublish(record, props); + await responseStream.WriteAsync(newPublishRecord); + } + + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error publishing records."); + } + } + + public PublishRecord PrepareRecordForPublish(dynamic record, RepeatedField props) + { + string data = string.Empty; + bool isInvalidRecord = false; + errorMsg = string.Empty; + var fullRecord = new List(); + + try + { + foreach (KeyValuePair col in record) + { + string typeName = NameToTypeConvert(props.Where(w => w.Name == col.Key.ToString()).Select(s => s.Type).FirstOrDefault()); + if (CanConvert(col.Value, Type.GetType(typeName))) + { + var convertedToType = Convert.ChangeType(col.Value, Type.GetType(typeName)); + if(convertedToType.GetType() == typeof(DateTime)) + { + fullRecord.Add(ToRfc3339String((DateTime)convertedToType)); + } + else + { + fullRecord.Add(convertedToType); + } + + } + else + { + fullRecord.Add(null); + isInvalidRecord = true; + } + } + + data = JsonSerializer.Serialize(fullRecord); + + } + catch (Exception ex) + { + _logger.LogError(ex, "Error preparing to publish record."); + } + + return new PublishRecord { Data = data, Invalid = isInvalidRecord, Error = errorMsg }; + } + + public bool CanConvert(object objToCast, Type type) + { + try + { + Convert.ChangeType(objToCast, type); + return true; + } + catch (Exception ex) + { + errorMsg = ex.Message; + _logger.LogWarning(ex, "Could not convert to desired data type."); + return false; + } + + } + + public static string NameToTypeConvert(string name) + { + switch (name) + { + case "integer": + return "System.Int32"; + case "number": + return "System.Decimal"; + case "datetime": + return "System.DateTime"; + case "boolean": + return "System.Boolean"; + case "string": + return "System.String"; + default: + return "System.String"; + } + } + + //source/adapted from https://sebnilsson.com/blog/c-datetime-to-rfc3339-iso-8601/ + public static string ToRfc3339String(DateTime dateTime) + { + return dateTime.ToString("yyyy-MM-dd'T'HH:mm:ss.fffZ", DateTimeFormatInfo.InvariantInfo); + } + + } +} diff --git a/NaveegoGrpcPlugin/NaveegoGrpcPlugin/Startup.cs b/NaveegoGrpcPlugin/NaveegoGrpcPlugin/Startup.cs new file mode 100644 index 0000000..e48315a --- /dev/null +++ b/NaveegoGrpcPlugin/NaveegoGrpcPlugin/Startup.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Serilog; + +namespace NaveegoGrpcPlugin +{ + public class Startup + { + private IConfiguration Configuration { get; } + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + public void ConfigureServices(IServiceCollection services) + { + services.AddGrpc(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime applicationLifetime) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + app.UseSerilogRequestLogging(); + + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapGrpcService(); + + endpoints.MapGet("/", async context => + { + await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); + }); + }); + + applicationLifetime.ApplicationStarted.Register(ShowPort); + + } + + private void ShowPort() + { + var port = Configuration.GetValue("GrpcPort").ToString(); + if (!string.IsNullOrEmpty(port)) + { + Console.WriteLine(port); + } + + } + } +} diff --git a/NaveegoGrpcPlugin/NaveegoGrpcPlugin/appsettings.Development.json b/NaveegoGrpcPlugin/NaveegoGrpcPlugin/appsettings.Development.json new file mode 100644 index 0000000..fe20c40 --- /dev/null +++ b/NaveegoGrpcPlugin/NaveegoGrpcPlugin/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Grpc": "Information", + "Microsoft": "Information" + } + } +} diff --git a/NaveegoGrpcPlugin/NaveegoGrpcPlugin/appsettings.json b/NaveegoGrpcPlugin/NaveegoGrpcPlugin/appsettings.json new file mode 100644 index 0000000..77bbd4d --- /dev/null +++ b/NaveegoGrpcPlugin/NaveegoGrpcPlugin/appsettings.json @@ -0,0 +1,9 @@ +{ + "AllowedHosts": "*", + "GrpcPort": "50051", + "Kestrel": { + "EndpointDefaults": { + "Protocols": "Http2" + } + } +} diff --git a/NaveegoGrpcPlugin/NaveegoGrpcPlugin/runtimeconfig.template.json b/NaveegoGrpcPlugin/NaveegoGrpcPlugin/runtimeconfig.template.json new file mode 100644 index 0000000..d43f4c0 --- /dev/null +++ b/NaveegoGrpcPlugin/NaveegoGrpcPlugin/runtimeconfig.template.json @@ -0,0 +1,5 @@ +{ + "configProperties": { + "System.Globalization.Invariant": true + } +}