private void BuildPackage(IApp app, string?gitSha, string?gitBranch, string folder) { // discover files to package var files = new List <KeyValuePair <string, string> >(); if (Directory.Exists(folder)) { foreach (var filePath in Directory.GetFiles(folder, "*", SearchOption.AllDirectories)) { var relativeFilePathName = Path.GetRelativePath(folder, filePath); // NOTE (2020-10-18, bjorg): skip 'appsettings.Development.json' file; since it's only useful for running locally and might contain sensitive information if (relativeFilePathName == AppSettingsDevelopmentJsonFileName) { continue; } files.Add(new KeyValuePair <string, string>(relativeFilePathName, filePath)); } files = files.OrderBy(file => file.Key).ToList(); } else { LogError($"cannot find folder '{folder}'"); return; } // compute hash for all files var fileValueToFileKey = files.ToDictionary(kv => kv.Value, kv => kv.Key); var hash = files.Select(kv => kv.Value).ComputeHashForFiles(file => fileValueToFileKey[file]); var package = Path.Combine(Provider.OutputDirectory, $"app_{Provider.ModuleFullName}_{app.LogicalId}_{hash}.zip"); // only build package if it doesn't exist if (!Provider.ExistingPackages.Remove(package)) { // check appsettings.json file exists var appSettingsFilepath = Path.Combine(folder, AppSettingsJsonFileName); var appSettingsExists = File.Exists(appSettingsFilepath); if (!appSettingsExists && ((gitSha != null) || (gitBranch != null))) { // add fake entry into list of files files.Add(new KeyValuePair <string, string>(AppSettingsJsonFileName, appSettingsFilepath)); } // package contents with built-in zip library using (var zipArchive = System.IO.Compression.ZipFile.Open(package, ZipArchiveMode.Create, new ForwardSlashEncoder())) { foreach (var file in files) { if (Provider.DetailedOutput) { Console.WriteLine($"... zipping: {file.Key}"); } // check if appsettings.json is being processed if (file.Key == AppSettingsJsonFileName) { var zipEntry = zipArchive.CreateEntry(file.Key); // check if appsettings.json exists string appSettingsText; if (appSettingsExists) { // read original file and attempt to augment it appSettingsText = File.ReadAllText(file.Value); var appSettings = JsonToNativeConverter.ParseObject(appSettingsText); if (appSettings != null) { AddLambdaSharpSettings(appSettings); appSettingsText = JsonSerializer.Serialize(appSettings, new JsonSerializerOptions { IgnoreNullValues = true, WriteIndented = false }); } } else { // create default appsettings.json file var appSettings = new Dictionary <string, object?>(); AddLambdaSharpSettings(appSettings); appSettingsText = JsonSerializer.Serialize(appSettings, new JsonSerializerOptions { IgnoreNullValues = true, WriteIndented = false }); } using (var zipStream = zipEntry.Open()) { zipStream.Write(Encoding.UTF8.GetBytes(appSettingsText)); } } else { zipArchive.CreateEntryFromFile(file.Value, file.Key); } } } } else { // update last write time on file File.SetLastWriteTimeUtc(package, DateTime.UtcNow); } Provider.AddArtifact($"{app.FullName}::PackageName", package); // local functions void AddLambdaSharpSettings(Dictionary <string, object?> appSettings) { var lambdaSharpSettings = new Dictionary <string, object?>(); if (gitSha != null) { lambdaSharpSettings["GitSha"] = gitSha; } if (gitBranch != null) { lambdaSharpSettings["GitBranch"] = gitBranch; } appSettings["LambdaSharp"] = lambdaSharpSettings; } }
public async Task CreateInvocationTargetSchemasAsync( string directory, string rootNamespace, IEnumerable <string> methodReferences, string outputFile ) { const string ASYNC_SUFFIX = "Async"; var schemas = new Dictionary <string, InvocationTargetDefinition>(); // create a list of nested namespaces from the root namespace var namespaces = new List <string>(); if (!string.IsNullOrEmpty(rootNamespace)) { var parts = rootNamespace.Split("."); for (var i = 0; i < parts.Length; ++i) { namespaces.Add(string.Join(".", parts.Take(i + 1)) + "."); } } namespaces.Add(""); namespaces.Reverse(); // enumerate type methods Console.WriteLine($"Inspecting method invocation targets in {directory}"); foreach (var methodReference in methodReferences.Distinct()) { InvocationTargetDefinition?entryPoint = null; try { // extract class and method names from method reference if (!StringEx.TryParseAssemblyClassMethodReference(methodReference, out var assemblyName, out var typeName, out var methodName)) { throw new ProcessTargetInvocationException($"method reference '{methodReference}' is not well formed"); } // load assembly Assembly assembly; var assemblyFilepath = Path.Combine(directory, assemblyName + ".dll"); try { assembly = Assembly.LoadFrom(assemblyFilepath); } catch (FileNotFoundException) { throw new ProcessTargetInvocationException($"could not find assembly '{assemblyFilepath}'"); } catch (Exception e) { throw new ProcessTargetInvocationException($"error loading assembly '{assemblyFilepath}': {e.Message}"); } // find type in assembly var type = namespaces.Select(ns => assembly.GetType(ns + typeName)).Where(t => t != null).FirstOrDefault(); if (type == null) { throw new ProcessTargetInvocationException($"could not find type for '{methodReference}' in assembly '{assembly.FullName}'"); } // find method, optionally with 'Async' suffix var method = type.GetMethod(methodName); if ((method == null) && !methodName.EndsWith(ASYNC_SUFFIX, StringComparison.Ordinal)) { methodName += ASYNC_SUFFIX; method = type.GetMethod(methodName); } if (method == null) { throw new ProcessTargetInvocationException($"could not find method '{methodName}' in type '{type.FullName}'"); } var resolvedMethodReference = $"{assemblyName}::{type.FullName}::{method.Name}"; var operationName = methodName.EndsWith(ASYNC_SUFFIX, StringComparison.Ordinal) ? methodName.Substring(0, methodName.Length - ASYNC_SUFFIX.Length) : methodName; // process method parameters ParameterInfo?requestParameter = null; ParameterInfo?proxyRequestParameter = null; var uriParameters = new List <KeyValuePair <string, bool> >(); var parameters = method.GetParameters(); foreach (var parameter in parameters) { // check if [FromUri] or [FromBody] attributes are present var customAttributes = parameter.GetCustomAttributes(true); var hasFromUriAttribute = customAttributes.Any(attribute => attribute.GetType().FullName == "LambdaSharp.ApiGateway.FromUriAttribute"); var hasFromBodyAttribute = customAttributes.Any(attribute => attribute.GetType().FullName == "LambdaSharp.ApiGateway.FromBodyAttribute"); if (hasFromUriAttribute && hasFromBodyAttribute) { throw new ProcessTargetInvocationException($"{resolvedMethodReference} parameter '{parameter.Name}' cannot have both [FromUri] and [FromBody] attributes"); } // check if parameter is a proxy request var isProxyRequest = (parameter.ParameterType.FullName == "Amazon.Lambda.APIGatewayEvents.APIGatewayProxyRequest") || (parameter.ParameterType.FullName == "Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest"); if (isProxyRequest) { if (hasFromUriAttribute || hasFromBodyAttribute) { throw new ProcessTargetInvocationException($"{resolvedMethodReference} parameter '{parameter.Name}' of type 'APIGatewayProxyRequest' or 'APIGatewayHttpApiV2ProxyRequest' cannot have [FromUri] or [FromBody] attribute"); } if (proxyRequestParameter != null) { throw new ProcessTargetInvocationException($"{resolvedMethodReference} parameters '{proxyRequestParameter.Name}' and '{parameter.Name}' conflict on proxy request"); } proxyRequestParameter = parameter; continue; } // check if parameter needs to deserialized from URI or BODY var isSimpleType = parameter.ParameterType.IsValueType || (parameter.ParameterType == typeof(string)); if ((isSimpleType && !hasFromBodyAttribute) || hasFromUriAttribute) { // check if parameter is read from URI string directly or if its members are read from the URI string if (isSimpleType) { // parameter is required only if it does not have an optional value and is not nullable uriParameters.Add(new KeyValuePair <string, bool>(parameter.Name ?? throw new InvalidOperationException("missing parameter name"), !parameter.IsOptional && (Nullable.GetUnderlyingType(parameter.ParameterType) == null) && (parameter.ParameterType.IsValueType || parameter.ParameterType == typeof(string)))); } else { var queryParameterType = parameter.ParameterType; // add complex-type properties foreach (var property in queryParameterType.GetProperties(BindingFlags.Instance | BindingFlags.Public)) { var name = property.GetCustomAttribute <System.Text.Json.Serialization.JsonPropertyNameAttribute>()?.Name ?? property.GetCustomAttribute <System.Runtime.Serialization.DataMemberAttribute>()?.Name ?? property.GetCustomAttribute <Newtonsoft.Json.JsonPropertyAttribute>()?.PropertyName ?? property.Name; var required = ( (Nullable.GetUnderlyingType(property.PropertyType) == null) && (property.PropertyType.IsValueType || (property.PropertyType == typeof(string))) && (property.GetCustomAttribute <Newtonsoft.Json.JsonPropertyAttribute>()?.Required != Newtonsoft.Json.Required.Default) && (property.GetCustomAttribute <Newtonsoft.Json.JsonPropertyAttribute>()?.Required != Newtonsoft.Json.Required.DisallowNull) ) || (property.GetCustomAttribute <System.ComponentModel.DataAnnotations.RequiredAttribute>() != null) || (property.GetCustomAttribute <Newtonsoft.Json.JsonRequiredAttribute>() != null) || (property.GetCustomAttribute <Newtonsoft.Json.JsonPropertyAttribute>()?.Required == Newtonsoft.Json.Required.Always) || (property.GetCustomAttribute <Newtonsoft.Json.JsonPropertyAttribute>()?.Required == Newtonsoft.Json.Required.AllowNull); uriParameters.Add(new KeyValuePair <string, bool>(name, required)); } // NOTE (2020-08-10, bjorg): System.Text.Json does not deserialize fields // add complex-type fields foreach (var field in queryParameterType.GetFields(BindingFlags.Instance | BindingFlags.Public)) { var name = field.GetCustomAttribute <Newtonsoft.Json.JsonPropertyAttribute>()?.PropertyName ?? field.Name; var required = ( (Nullable.GetUnderlyingType(field.FieldType) == null) && (field.FieldType.IsValueType || (field.FieldType == typeof(string))) && (field.GetCustomAttribute <Newtonsoft.Json.JsonPropertyAttribute>()?.Required != Newtonsoft.Json.Required.Default) && (field.GetCustomAttribute <Newtonsoft.Json.JsonPropertyAttribute>()?.Required != Newtonsoft.Json.Required.DisallowNull) ) || (field.GetCustomAttribute <Newtonsoft.Json.JsonRequiredAttribute>() != null) || (field.GetCustomAttribute <Newtonsoft.Json.JsonPropertyAttribute>()?.Required == Newtonsoft.Json.Required.Always) || (field.GetCustomAttribute <Newtonsoft.Json.JsonPropertyAttribute>()?.Required == Newtonsoft.Json.Required.AllowNull); uriParameters.Add(new KeyValuePair <string, bool>(name, required)); } } } else { if (requestParameter != null) { throw new ProcessTargetInvocationException($"{resolvedMethodReference} parameters '{requestParameter.Name}' and '{parameter.Name}' conflict on request body"); } requestParameter = parameter; } } // check if no specific request parameter was present, but the method also takes a proxy request if ((requestParameter == null) && (proxyRequestParameter != null)) { requestParameter = proxyRequestParameter; } // process method request type var requestSchemaAndContentType = await AddSchema(methodReference, $"for parameter '{requestParameter?.Name}'", requestParameter?.ParameterType); // process method response type var responseType = (method.ReturnType.IsGenericType) && (method.ReturnType.GetGenericTypeDefinition() == typeof(Task <>)) ? method.ReturnType.GetGenericArguments()[0] : method.ReturnType; var responseSchemaAndContentType = await AddSchema(method.Name, "as return value", responseType); entryPoint = new InvocationTargetDefinition { Assembly = assemblyName, Type = type.FullName, Method = methodName, OperationName = operationName, RequestContentType = requestSchemaAndContentType?.Item2, RequestSchema = requestSchemaAndContentType?.Item1, RequestSchemaName = requestParameter?.ParameterType.FullName, UriParameters = uriParameters.Any() ? new Dictionary <string, bool>(uriParameters) : null, ResponseContentType = responseSchemaAndContentType?.Item2, ResponseSchema = responseSchemaAndContentType?.Item1, ResponseSchemaName = responseType?.FullName }; // write result Console.WriteLine($"... {resolvedMethodReference}({string.Join(", ", uriParameters.Select(kv => kv.Key))}) {entryPoint.GetRequestSchemaName()} -> {entryPoint.GetResponseSchemaName()}"); } catch (ProcessTargetInvocationException e) { entryPoint = new InvocationTargetDefinition { Error = e.Message }; } catch (Exception e) { entryPoint = new InvocationTargetDefinition { Error = $"internal error: {e.Message}", StackTrace = e.StackTrace }; } if (entryPoint != null) { schemas.Add(methodReference, entryPoint); } else { schemas.Add(methodReference, new InvocationTargetDefinition { Error = "internal error: missing target definition" }); } } // create json document try { var output = JsonSerializer.Serialize(schemas, new JsonSerializerOptions { IgnoreNullValues = false, WriteIndented = true }); if (outputFile != null) { File.WriteAllText(outputFile, output); } else { Console.WriteLine(output); } } catch (Exception e) { LogError("unable to write schema", e); } // local functions async Task <Tuple <object, string?> > AddSchema(string methodReference, string parameterName, Type?messageType) { // check if there is no request type if (messageType == null) { return(Tuple.Create((object)"Void", (string?)null)); } // check if there is no response type if ( (messageType == typeof(void)) || (messageType == typeof(Task)) ) { return(Tuple.Create((object)"Void", (string?)null)); } // check if request/response type is not supported if ( (messageType == typeof(string)) || messageType.IsValueType ) { throw new ProcessTargetInvocationException($"{methodReference} has unsupported type {parameterName}"); } // check if request/response type is inside 'Task<T>' if (messageType.IsGenericType && messageType.GetGenericTypeDefinition() == typeof(Task <>)) { messageType = messageType.GetGenericArguments()[0]; } // check if request/response has an open-ended schema if ( (messageType == typeof(object)) || (messageType == typeof(Newtonsoft.Json.Linq.JObject)) ) { return(Tuple.Create((object)"Object", (string?)"application/json")); } // check if request/response is not a proxy request/response if ( (messageType.FullName != "Amazon.Lambda.APIGatewayEvents.APIGatewayProxyRequest") && (messageType.FullName != "Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse") && (messageType.FullName != "Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest") && (messageType.FullName != "Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse") ) { var schema = await JsonSchema4.FromTypeAsync(messageType, new JsonSchemaGeneratorSettings { FlattenInheritanceHierarchy = true, #pragma warning disable CS0618 // we prefer enums to be handled as strings (NOTE: trying to set this in SerializerSettings causes an NRE in JsonSchema4FromTypeAsync call) DefaultEnumHandling = EnumHandling.String #pragma warning restore CS0618 }); // NOTE (2019-04-03, bjorg): we need to allow additional properties, because Swagger doesn't support: "additionalProperties": false schema.AllowAdditionalProperties = true; foreach (var definition in schema.Definitions) { definition.Value.AllowAdditionalProperties = true; } // NOTE (2019-08-16, bjorg): don't emit "x-enumNames" as it is not supported by API Gateway foreach (var definition in schema.Definitions) { definition.Value.EnumerationNames = null; } // return JSON schema document return(Tuple.Create((object)(JsonToNativeConverter.ParseObject(schema.ToJson()) ?? throw new InvalidDataException("schema is not a valid JSON object")), (string?)"application/json")); } return(Tuple.Create((object)"Proxy", (string?)null)); } }
//--- Methods --- public void Build( IApp app, bool noCompile, bool noAssemblyValidation, string?gitSha, string?gitBranch, string buildConfiguration, bool forceBuild, out string platform, out string framework, out string appVersionId ) { // read settings from project file var projectFile = new CSharpProjectFile(app.Project); var targetFramework = projectFile.TargetFramework; // set output parameters platform = "Blazor WebAssembly"; framework = targetFramework; // check if any app files are newer than the most recently built package; otherwise, skip build var appMetadataFilepath = Path.Combine(Provider.OutputDirectory, $"appmetadata_{Provider.ModuleFullName}_{app.LogicalId}.json"); if (!forceBuild) { var appPackage = Provider.ExistingPackages.FirstOrDefault(p => Path.GetFileName(p).StartsWith($"app_{Provider.ModuleFullName}_{app.LogicalId}_", StringComparison.Ordinal) && p.EndsWith(".zip", StringComparison.Ordinal) ); if ((appPackage != null) && File.Exists(appMetadataFilepath)) { LogInfoVerbose($"=> Analyzing app {app.FullName} dependencies"); // find all files used to create the app package var files = new HashSet <string>(); CSharpProjectFile.DiscoverDependencies( files, app.Project, filePath => LogInfoVerbose($"... analyzing {filePath}"), (message, exception) => LogError(message, exception) ); // check if any of the files has been modified more recently than the app package var appPackageDate = File.GetLastWriteTime(appPackage); var file = files.FirstOrDefault(f => File.GetLastWriteTime(f) > appPackageDate); if (file == null) { try { // attempt to load extract assembly metadata var appMetadata = JsonSerializer.Deserialize <LambdaSharpTool.AssemblyMetadata>(File.ReadAllText(appMetadataFilepath)); if (appMetadata.ModuleVersionId != null) { Provider.WriteLine($"=> Skipping app {Provider.InfoColor}{app.FullName}{Provider.ResetColor} (no changes found)"); // keep the existing files Provider.ExistingPackages.Remove(appMetadataFilepath); Provider.ExistingPackages.Remove(appPackage); // set the module variable to the final package name appVersionId = appMetadata.ModuleVersionId; Provider.AddArtifact($"{app.FullName}::PackageName", appPackage); return; } } catch { // ignore exception and continue } } else { LogInfoVerbose($"... found newer file: {file}"); } } } else { LogInfoVerbose($"=> Analyzing app {app.FullName} dependencies"); // find all files used to create the app package var files = new HashSet <string>(); CSharpProjectFile.DiscoverDependencies( files, app.Project, filePath => LogInfoVerbose($"... analyzing {filePath}"), (message, exception) => LogError(message, exception) ); // loop over all project folders new CleanBuildFolders(BuildEventsConfig).Do(files); } // validate the project is using the most recent lambdasharp assembly references if ( !noAssemblyValidation && app.HasAssemblyValidation && !projectFile.ValidateLambdaSharpPackageReferences(Provider.ToolVersion, LogWarn, LogError) ) { appVersionId = "<MISSING>"; return; } if (noCompile) { appVersionId = "<MISSING>"; return; } // compile app project Provider.WriteLine($"=> Building app {Provider.InfoColor}{app.FullName}{Provider.ResetColor} [{projectFile.TargetFramework}, {buildConfiguration}]"); var projectDirectory = Path.Combine(Provider.WorkingDirectory, Path.GetFileNameWithoutExtension(app.Project)); if (File.Exists(appMetadataFilepath)) { File.Delete(appMetadataFilepath); } // build and publish Blazor app if (!DotNetBuildBlazor(projectFile.TargetFramework, buildConfiguration, projectDirectory)) { // nothing to do; error was already reported appVersionId = "<MISSING>"; return; } // extract version id from app var assemblyFilepath = (string.Compare(targetFramework, "netstandard2.1", StringComparison.Ordinal) == 0) ? Path.Combine(projectDirectory, "bin", buildConfiguration, targetFramework, "publish", "wwwroot", "_framework", "_bin", $"{Path.GetFileNameWithoutExtension(app.Project)}.dll") : Path.Combine(projectDirectory, "bin", buildConfiguration, targetFramework, "publish", "wwwroot", "_framework", $"{Path.GetFileNameWithoutExtension(app.Project)}.dll"); var assemblyMetadata = LambdaSharpAppAssemblyInformation(assemblyFilepath, appMetadataFilepath); if (assemblyMetadata?.ModuleVersionId == null) { LogError($"unable to extract assembly metadata"); appVersionId = "<MISSING>"; return; } Provider.ExistingPackages.Remove(appMetadataFilepath); LogInfoVerbose($"... assembly version id: {assemblyMetadata.ModuleVersionId}"); appVersionId = assemblyMetadata.ModuleVersionId; // update `blazor.boot.json` file by adding `appsettings.Production.json` to config list var wwwRootFolder = Path.Combine(projectDirectory, "bin", buildConfiguration, targetFramework, "publish", "wwwroot"); if (File.Exists(Path.Combine(wwwRootFolder, AppSettingsProductionJsonFileName))) { LogError($"'{AppSettingsProductionJsonFileName}' is reserved for loading deployment generated configuration settings and cannot be used explicitly"); return; } var blazorBootJsonFileName = Path.Combine(wwwRootFolder, "_framework", "blazor.boot.json"); var blazorBootJson = JsonToNativeConverter.ParseObject(File.ReadAllText(blazorBootJsonFileName)); if ( (blazorBootJson != null) && (blazorBootJson.TryGetValue("config", out var blazorBootJsonConfig)) && (blazorBootJsonConfig is List <object?> blazorBootJsonConfigList) ) { var blazorBootJsonModified = false; if (!blazorBootJsonConfigList.Contains(AppSettingsJsonFileName) && ((gitSha != null) || (gitBranch != null))) { // add instruction to load appsettings.json blazorBootJsonConfigList.Add(AppSettingsJsonFileName); blazorBootJsonModified = true; } if (!blazorBootJsonConfigList.Contains(AppSettingsProductionJsonFileName)) { // add instruction to load appsettings.Production.json blazorBootJsonConfigList.Add(AppSettingsProductionJsonFileName); blazorBootJsonModified = true; } if (blazorBootJsonModified) { LogInfoVerbose("... updating 'blazor.boot.json' configuration file"); File.WriteAllText(blazorBootJsonFileName, JsonSerializer.Serialize(blazorBootJson)); } } else { LogError($"unable to update {blazorBootJsonFileName}"); } // zip output folder BuildPackage(app, gitSha, gitBranch, wwwRootFolder); }