public override bool CanResolve(Entity owner, string key, bool fallback = false) { return((key == Constants.OutputArgumentName && CommandEntity.Decorate(owner).CommandName.Equals("deploy", StringComparison.OrdinalIgnoreCase)) || (key == EntityKeys.InternalDeployPathKey && TargetEntity.Decorate(owner).HasFullName)); }
public override Entity Resolve(Entity owner, string key, bool fallback = false) { if (key == EntityKeys.InternalDeployPathKey) { TargetEntity targetEntity = TargetEntity.Decorate(owner); BuildEntity buildEntity = BuildEntity.Decorate(owner); Entity project = owner.Root; CommandEntity commandOrigin = CommandEntity.Decorate(owner.Origin); VirtualDirectory outputRoot = fileSystem.GetDirectory(commandOrigin.Output, project.Path); VirtualDirectory deployRoot = outputRoot.Directory(targetEntity.FullName.Replace(',', '_'), buildEntity.BuildType); return(owner.Create(key, deployRoot.FullName, deployRoot)); } CommandEntity command = CommandEntity.Decorate(owner); FileEntity projectFileEntity = FileEntity.Decorate(owner.Root); string outputDirectory = command.GetSingleValueArgument(Constants.OutputArgumentName); if (string.IsNullOrEmpty(outputDirectory)) { outputDirectory = projectFileEntity.Directory .Directory(Constants.LibraryFolderName) .FullName; } return(owner.Create(key, outputDirectory)); }
private static string GetBuildType(Entity owner) { CommandEntity commandEntity = CommandEntity.Decorate(owner.Origin); string buildType = commandEntity.GetSingleValueArgument(EntityKeys.BuildTypeKey); if (string.IsNullOrEmpty(buildType)) { buildType = Constants.ReleaseFolderName; } return(buildType); }
public override Entity Resolve(Entity owner, string key, bool fallback = false) { switch (key) { case EntityKeys.ProjectConfigurationsKey: return(GetProjectConfiguration()); case EntityKeys.EngineerVersionKey: return(GetEngineerVersion()); case EntityKeys.SolutionVersionKey: return(GetSolutionVersion()); case EntityKeys.LibraryDescriptionKey: return(GetLibraryDescription()); case EntityKeys.LibraryVersionKey: return(GetLibraryVersion()); default: throw new ContentProviderException(key, owner); } Entity GetProjectConfiguration() { if (!owner.HasPath) { executionContext.WriteVerbose("The executed command lacks the path argument, no project configuration can be loaded."); return(owner.Create(key, new ProjectConfigurations())); } string rootFilePath = fileSystem.GetDirectory(owner.Path, createNew: false) .FullName; string projectDirectory = fileSystem.FileExists(rootFilePath) ? Path.GetDirectoryName(rootFilePath) : rootFilePath; VirtualFile file = fileSystem.DirectoryExists(projectDirectory) && fileSystem.FileExists(Path.Combine(projectDirectory, Constants.ConfigFileName)) ? fileSystem.GetFile(Path.Combine(projectDirectory, Constants.ConfigFileName)) : null; if (file == null) { executionContext.WriteVerbose($"No config file found in {projectDirectory}."); return(owner.Create(key, new ProjectConfigurations())); } try { using (Stream fileStream = file.OpenRead()) using (XmlReader reader = XmlReader.Create(fileStream)) { XmlSerializer serializer = new XmlSerializer(typeof(ProjectConfiguration)); ProjectConfiguration configuration = (ProjectConfiguration)serializer.Deserialize(reader); if (configuration != null) { return(owner.Create(key, new ProjectConfigurations(configuration, file))); } } } catch (DeployArgumentsException) { throw; } catch (Exception e) { executionContext.WriteVerbose($"Error while trying to parse project configuration {file.FullName}." + $"{Environment.NewLine}{e}"); } return(owner.Create(key, new ProjectConfigurations())); } Entity GetEngineerVersion() { CommandEntity command = CommandEntity.Decorate(owner.Origin); ProjectEntity project = ProjectEntity.Decorate(owner); if (command.CommandName.Equals("deploy", StringComparison.OrdinalIgnoreCase) && command.IsCommandArgumentSpecified(Constants.EngineerVersionArgumentKey)) { if (command.IsCommandArgumentSpecified(Constants.SolutionVersionArgumentKey)) { throw new DeployArgumentsException(); } string value = command.GetSingleValueArgument(Constants.EngineerVersionArgumentKey); project.Configuration.EngineerVersion = value; return(owner.Create(key, value)); } return(owner.Create(key, project.Configuration.EngineerVersion)); } Entity GetSolutionVersion() { CommandEntity command = CommandEntity.Decorate(owner.Origin); ProjectEntity project = ProjectEntity.Decorate(owner); if (command.CommandName.Equals("deploy", StringComparison.OrdinalIgnoreCase) && command.IsCommandArgumentSpecified(Constants.SolutionVersionArgumentKey)) { if (command.IsCommandArgumentSpecified(Constants.EngineerVersionArgumentKey)) { throw new DeployArgumentsException(); } string value = command.GetSingleValueArgument(Constants.SolutionVersionArgumentKey); project.Configuration.SolutionVersion = value; return(owner.Create(key, value)); } return(owner.Create(key, project.Configuration.SolutionVersion)); } Entity GetLibraryDescription() { CommandEntity command = CommandEntity.Decorate(owner.Origin); ProjectEntity project = ProjectEntity.Decorate(owner); if (command.CommandName.Equals("deploy", StringComparison.OrdinalIgnoreCase) && command.IsCommandArgumentSpecified(Constants.DescriptionArgumentKey)) { string value = command.GetSingleValueArgument(Constants.DescriptionArgumentKey); project.Configuration.LibraryDescription = value; return(owner.Create(key, value)); } return(owner.Create(key, project.Configuration.LibraryDescription)); } Entity GetLibraryVersion() { CommandEntity command = CommandEntity.Decorate(owner.Origin); ProjectEntity project = ProjectEntity.Decorate(owner); if (command.CommandName.Equals("deploy", StringComparison.OrdinalIgnoreCase) && command.IsCommandArgumentSpecified(Constants.VersionArgumentKey)) { string value = command.GetSingleValueArgument(Constants.VersionArgumentKey); project.Configuration.LibraryVersion = value; return(owner.Create(key, value)); } return(owner.Create(key, project.Configuration.LibraryVersion)); } }
public override Entity Resolve(Entity owner, string key, bool fallback = false) { switch (key) { case EntityKeys.TargetFullNameKey: return(owner.Create(key, owner.Value <Target>().GetFullName())); case EntityKeys.TargetShortFullNameKey: return(owner.Create(key, owner.Value <Target>().GetShortFullName())); case EntityKeys.TargetEngineerVersionKey: return(owner.Create(key, EngineerVersion(owner.Value <Target>()))); case EntityKeys.TargetVersionKey: Version version = new Version(); string versionString = owner.Value <Target>().Version; if (Version.TryParse(versionString, out Version parsedVersion)) { version = parsedVersion; } return(owner.Create(key, version)); case EntityKeys.NameKey: return(owner.Create(key, owner.Value <Target>().Name)); default: return(GetTargets(key == EntityKeys.ValidatedTargetsKey)); } Entity GetTargets(bool validate) { CommandEntity commandEntity = CommandEntity.Decorate(owner.Origin); if (commandEntity.IsCommandArgumentSpecified(Constants.TargetArgumentName)) { IEnumerable <string> targets = commandEntity.GetMultiValueArgument(Constants.TargetArgumentName); //TODO parseLocation true here is legacy as soon as old generate library command is gone, reset to false IEnumerable <Target> targetsSet = GetSpecificTargets(targets, validate, true).Select(t => t.Item1); return(owner.Create(key, targetsSet.Select(t => owner.Create(key.Singular(), t.Name, t)))); } ProjectEntity project = ProjectEntity.Decorate(owner); TargetsResult result = Targets(project, validate); return(owner.Create(key, result.ValidTargets.Select(t => owner.Create(key.Singular(), t.Name, t)))); } string EngineerVersion(Target target) { string possibleVersion = target.LongVersion.Trim().Split(' ')[0]; if (Version.TryParse(possibleVersion, out Version version)) { int parts = possibleVersion.Split('.').Length; if (parts == 2) { return($"{version.ToString(2)}.0"); } if (parts > 2) { return(version.ToString(3)); } } return(target.ShortVersion); } }
public void DeployFiles(Entity dataModel) { IEnumerable <Entity> deployableEntities = dataModel.Root.Hierarchy(); ProjectEntity project = ProjectEntity.Decorate(dataModel.Root); // get targets to deploy for IEnumerable <Target> targets = null; CommandEntity command = CommandEntity.Decorate(dataModel); if (command.IsCommandArgumentSpecified(Constants.TargetArgumentName)) { IEnumerable <string> rawTargets = command.GetMultiValueArgument(Constants.TargetArgumentName); if (rawTargets.Any()) { targets = targetParser.GetSpecificTargets(rawTargets, false).Select(t => t.Item1); } else { targets = targetParser.Targets(project, false).ValidTargets; } } else { targets = targetParser.Targets(project, false).ValidTargets; } if (!targets.Any()) { throw new NoTargetSpecifiedException(); } foreach (Entity deployableEntity in deployableEntities) { DeployFilesFromTemplate(deployableEntity); } if (command.IsCommandArgumentSpecified(Constants.FilesArgumentName)) { IEnumerable <string> files = command.GetMultiValueArgument(Constants.FilesArgumentName); foreach (string file in files) { DeployFileFromArgument(file); } } void DeployFileFromArgument(string file) { Match match = FilesDecoder.Match(file); if (!match.Success) { throw new FormattableException($"The input {file} could not be parsed. Expected pattern is <fileLocation>|<destination>[|<target>]"); } string from = match.Groups["from"].Value; string to = match.Groups["destination"].Value; string rawTarget = match.Groups["target"].Value; VirtualDirectory baseDirectory = null; string relativePath = null; bool recreateStructure = false; string[] path = fileSystem.GetPath(from); int firstWildCard = path.TakeWhile(p => !p.Contains('*') && !p.Contains('?')).Count(); if (firstWildCard != path.Length) { baseDirectory = fileSystem.GetDirectory(Path.Combine(path.Take(firstWildCard).ToArray()), project.Path, false); relativePath = Path.Combine(path.Skip(firstWildCard).ToArray()); recreateStructure = true; } if (recreateStructure) { IEnumerable <VirtualFile> deployFiles = baseDirectory.Files(relativePath, true).ToArray(); if (!deployFiles.Any()) { throw new DeployFileNotFoundException(@from); } foreach (VirtualFile deployFile in deployFiles) { string structure = Path.GetDirectoryName(deployFile.GetRelativePath(baseDirectory)); string fileDestination = string.IsNullOrEmpty(structure) ? to : Path.Combine(to, structure); DeployFile(deployFile.FullName, fileDestination); } } else if (fileSystem.FileExists(from, project.Path)) { DeployFile(from, to); } else { throw new DeployFileNotFoundException(@from); } void DeployFile(string sourceFile, string destinationDirectory) { if (!string.IsNullOrEmpty(rawTarget)) { DeployFileForRawTarget(rawTarget, sourceFile, destinationDirectory); } else { foreach (Target target in targets) { DeployFileForTarget(target, sourceFile, destinationDirectory); } } } void DeployFileForRawTarget(string target, string sourceFile, string destinationDirectory) { Target parsedTarget = targetParser.ParseTarget(target, null, targets); DeployFileForTarget(parsedTarget, sourceFile, destinationDirectory); } void DeployFileForTarget(Target target, string sourceFile, string destinationDirectory) { VirtualFile fileToCopy = fileSystem.GetFile(sourceFile, project.Path); VirtualFile copiedFile = fileSystem.GetDirectory(destinationDirectory, GetOutputDirectory(target).FullName).File(fileToCopy.Name); using (Stream source = fileToCopy.OpenRead(true)) using (Stream destination = copiedFile.OpenWrite()) { destination.SetLength(0); source.CopyTo(destination); executionContext.WriteVerbose($"Deployed file {fileToCopy.FullName} to {copiedFile.FullName}."); } } } void DeployFilesFromTemplate(Entity deployableEntity) { TemplateDescription template = deployableEntity.Template(); if (template == null) { return; } foreach (templateFile file in template.File) { if (!file.deployPathSpecified) { continue; } VirtualFile deployableFile = GetFile(file, dataModel.Root.Path, false, out string path); DeployFile(file, deployableFile, path); } foreach (templateGeneratedFile generatedFile in template.GeneratedFile ?? Enumerable.Empty <templateGeneratedFile>()) { if (!generatedFile.deployPathSpecified) { continue; } VirtualFile deployableFile = GetFile(generatedFile, dataModel.Root.Path, true, out string path); DeployFile(generatedFile, deployableFile, path); } VirtualFile GetDestination(templateFile file, Target target, string name) { string basePath = GetOutputDirectory(target).FullName; string path = resolver.Resolve(file.deployPath ?? string.Empty, deployableEntity); VirtualFile destination = fileSystem.GetFile(Path.Combine(path, name), basePath); return(destination); } void DeployFile(templateFile file, VirtualFile deployableFile, string filePath) { if (deployableFile == null) { executionContext.WriteVerbose($"Could not find file {filePath} in {dataModel.Root.Path}, the file will not be deployed."); return; } foreach (Target target in targets) { VirtualFile destination = GetDestination(file, target, deployableFile.Name); using (Stream source = deployableFile.OpenRead(true)) using (Stream dest = destination.OpenWrite()) { dest.SetLength(0); source.CopyTo(dest); } executionContext.WriteVerbose($"Deployed file {deployableFile.FullName} to {destination.FullName}."); } } VirtualFile GetFile(templateFile file, string basePath, bool isGeneratedFile, out string realFilePath) { string path = resolver.Resolve(file.path ?? string.Empty, deployableEntity); string name = resolver.Resolve(file.name, deployableEntity); if (isGeneratedFile && file is templateGeneratedFile generatedFile) { realFilePath = Path.Combine(path, name); if (!Path.IsPathRooted(realFilePath)) { realFilePath = Path.Combine(Constants.IntermediateFolderName, generatedFile.generator?.ToLowerInvariant() ?? string.Empty, realFilePath); } } else { realFilePath = Path.Combine(path, name); } VirtualFile destination = fileSystem.FileExists(realFilePath, basePath) ? fileSystem.GetFile(realFilePath, basePath) : null; return(destination); } } VirtualDirectory GetOutputDirectory(Target target) { string buildTypeFolder = command.IsCommandArgumentSpecified(Constants.BuildTypeArgumentName) ? FormatBuildType(command.GetSingleValueArgument(Constants.BuildTypeArgumentName)) : Constants.ReleaseFolderName; string basePath = project.Path; if (!command.IsCommandArgumentSpecified(Constants.OutputArgumentName)) { return(fileSystem.GetDirectory(Path.Combine(basePath, Constants.LibraryFolderName, target.GetFullName().Replace(',', '_'), buildTypeFolder))); } basePath = fileSystem.GetDirectory(command.GetSingleValueArgument(Constants.OutputArgumentName), basePath).FullName; basePath = Path.Combine(basePath, target.GetFullName().Replace(',', '_'), buildTypeFolder); return(fileSystem.GetDirectory(basePath)); string FormatBuildType(string buildType) { if (string.IsNullOrEmpty(buildType)) { return(Constants.ReleaseFolderName); } return(buildType.Substring(0, 1).ToUpperInvariant() + buildType.Substring(1).ToLowerInvariant()); } } }
private string GenerateCommandOptions(ProjectEntity project, Dictionary <Entity, VirtualFile> projectLibraries, string projectName) { FileEntity projectFileEntity = FileEntity.Decorate(project); VirtualFile commandOptions = projectFileEntity.TempDirectory.File("CommandOptions.txt"); CommandEntity commandOrigin = CommandEntity.Decorate(project.Origin); VirtualDirectory outputRoot = fileSystem.GetDirectory(commandOrigin.Output, project.Path, false); List <string> processedMetaFiles = new List <string>(); executionContext.Observable.OnNext(new Change(() => { }, $"Create command options file {commandOptions.FullName}")); using (Stream stream = commandOptions.OpenWrite()) using (StreamWriter writer = new StreamWriter(stream)) { writer.WriteLine($"{Constants.OutputOption} \"{MakeRelative(Path.Combine(outputRoot.FullName, projectName))}.{Constants.EngineeringLibraryExtension}\""); writer.WriteLine($"{Constants.GuidOption} {project.Id:D}"); RenameAndWriteLibraryFile(writer); WriteMetadata(writer); AddAdditionalFiles(writer); } return(commandOptions.FullName); void RenameAndWriteLibraryFile(StreamWriter writer) { foreach (TargetEntity target in projectLibraries.Keys.Select(TargetEntity.Decorate)) { VirtualFile renamedLibrary = projectFileEntity .TempDirectory.Directory(target.FullName.Replace(",", "_")) .File("lib" + projectName + Path.GetExtension(projectLibraries[target.Base].Name)); executionContext.Observable.OnNext(new Change(() => { }, $"Rename library file to {renamedLibrary.FullName}")); using (Stream source = projectLibraries[target.Base].OpenRead(true)) using (Stream destination = renamedLibrary.OpenWrite()) { source.CopyTo(destination); } writer.WriteLine(string.Format(CultureInfo.InvariantCulture, Constants.PlcnextNativeLibraryOptionPattern, renamedLibrary.Parent.FullName, target.Name, target.EngineerVersion, guidFactory.Create().ToString("D", CultureInfo.InvariantCulture), target.ShortFullName.Replace(",", "_"))); } } void AddAdditionalFiles(StreamWriter writer) { foreach (Entity target in projectLibraries.Keys) { VirtualDirectory deployDirectory = DeployEntity.Decorate(target).DeployDirectory; IEnumerable <VirtualFile> files = deployDirectory .Files(searchRecursive: true).Except(projectLibraries.Values) .Where(f => !processedMetaFiles.Contains(f.GetRelativePath(deployDirectory))); foreach (VirtualFile file in files) { writer.WriteLine(string.Format(CultureInfo.InvariantCulture, "/file \":{0}:{1}\"", file.GetRelativeOrAbsolutePath(projectFileEntity.Directory), TargetEntity.Decorate(target).ShortFullName.Replace(",", "_"))); } } } void WriteMetadata(StreamWriter writer) { VirtualDirectory deployDirectory = DeployEntity.Decorate(projectLibraries.Keys.First()).DeployDirectory; HashSet <VirtualDirectory> createDirectories = new HashSet <VirtualDirectory>(); foreach (VirtualFile metaFile in deployDirectory.Files(searchRecursive: true)) { string destinationPath; string fileType; switch (Path.GetExtension(metaFile.Name)?.ToUpperInvariant() ?? string.Empty) { case ".LIBMETA": destinationPath = string.Empty; fileType = Constants.LibmetaFileType; break; case ".TYPEMETA": destinationPath = string.Empty; fileType = Constants.TypemetaFileType; break; case ".COMPMETA": CreateComponentDirectory(metaFile.Parent); destinationPath = metaFile.Parent.Name; fileType = Constants.CompmetaFileType; break; case ".PROGMETA": CreateProgramDirectory(metaFile.Parent); destinationPath = $"{metaFile.Parent.Parent.Name}/{metaFile.Parent.Name}"; fileType = Constants.ProgmetaFileType; break; default: //do nothing all other files are not interesting continue; } writer.WriteLine(string.Format(CultureInfo.InvariantCulture, Constants.FileOptionPattern, fileType, MakeRelative(metaFile.FullName), guidFactory.Create().ToString("D", CultureInfo.InvariantCulture), destinationPath)); processedMetaFiles.Add(metaFile.GetRelativePath(deployDirectory)); } void CreateComponentDirectory(VirtualDirectory componentDirectory) { if (createDirectories.Contains(componentDirectory)) { return; } writer.WriteLine(string.Format(CultureInfo.InvariantCulture, Constants.DirectoryOptionPattern, $"Logical Elements/{componentDirectory.Name}", Constants.ComponentFolderType, guidFactory.Create().ToString("D", CultureInfo.InvariantCulture))); createDirectories.Add(componentDirectory); } void CreateProgramDirectory(VirtualDirectory programDirectory) { if (createDirectories.Contains(programDirectory)) { return; } CreateComponentDirectory(programDirectory.Parent); writer.WriteLine(string.Format(CultureInfo.InvariantCulture, Constants.DirectoryOptionPattern, $"Logical Elements/{programDirectory.Parent.Name}/{programDirectory.Name}", Constants.ProgramFolderType, guidFactory.Create().ToString("D", CultureInfo.InvariantCulture))); createDirectories.Add(programDirectory); } } string MakeRelative(string path) { return(path.GetRelativePath(projectFileEntity.Directory.FullName)); } }
public override Entity Resolve(Entity owner, string key, bool fallback = false) { switch (key) { case EntityKeys.PathKey: return(GetProjectPath()); case EntityKeys.NameKey: return(GetProjectName()); case EntityKeys.ProjectSettingsKey: return(GetProjectSettings()); case EntityKeys.ProjectVersionKey: return(GetProjectVersion()); case EntityKeys.ProjectIdKey: return(GetProjectId()); default: throw new ContentProviderException(key, owner); } Entity GetProjectId() { CommandEntity command = CommandEntity.Decorate(owner.Origin); if (command.IsCommandArgumentSpecified(Constants.IdArgumentName)) { string id = command.GetSingleValueArgument(Constants.IdArgumentName); if (!Guid.TryParse(id, out Guid guid)) { throw new LibraryIdMalformattedException(id); } return(owner.Create(key, guid, guid.ToString("D", CultureInfo.InvariantCulture))); } ProjectEntity project = ProjectEntity.Decorate(owner); if (!project.Settings.IsPersistent) { executionContext.WriteWarning("The id for the library will change for each generation please use the --id option to set the id."); Guid id = guidFactory.Create(); return(owner.Create(key, id, id.ToString("D", CultureInfo.InvariantCulture))); } string storedId = project.Settings.Value.Id; if (string.IsNullOrEmpty(storedId)) { storedId = guidFactory.Create().ToString("D", CultureInfo.InvariantCulture); project.Settings.SetId(storedId); } Guid result = Guid.Parse(storedId); return(owner.Create(key, result, result.ToString("D", CultureInfo.InvariantCulture))); } Entity GetProjectPath() { ProjectDescription description = owner.Value <ProjectDescription>(); VirtualDirectory path = description?.Root ?? fileSystem.CurrentDirectory; return(owner.Create(key, path.FullName, path)); } Entity GetProjectSettings() { ProjectDescription description = owner.Value <ProjectDescription>(); return(owner.Create(key, new MutableProjectSettings(description.Settings, description.File, executionContext))); } Entity GetProjectName() { ProjectDescription description = owner.Value <ProjectDescription>(); return(owner.Create(key, description?.Root.Name ?? Path.GetFileName(owner.Path))); } Entity GetProjectVersion() { ProjectDescription description = owner.Value <ProjectDescription>(); return(owner.Create(key, Version.Parse(description.Settings.Version))); } }
public override Entity Resolve(Entity owner, string key, bool fallback = false) { if (key == EntityKeys.IncludeKey && owner.Type == EntityKeys.FormatKey) { return(ResolveInclude()); } throw new ContentProviderException(key, owner); Entity ResolveInclude() { IEntityBase templateOrigin = GetTemplateOrigin(out IEntityBase formatOrigin); CommandEntity commandEntity = CommandEntity.Decorate(owner.Origin); string basePath = string.Empty; if (templateOrigin.HasContent(EntityKeys.BaseDirectoryKey) && !string.IsNullOrEmpty(templateOrigin[EntityKeys.BaseDirectoryKey].Value <string>())) { string path = templateOrigin.Path; string relative = path.Split(new[] { templateOrigin[EntityKeys.BaseDirectoryKey].Value <string>() }, StringSplitOptions.None) .Last(); Match match = splitRegex.Match(relative); if (match.Success && match.Groups["split"].Success) { relative = match.Groups["split"].Value; } basePath = relative.Replace('\\', '/'); } else if (commandEntity.IsCommandArgumentSpecified(EntityKeys.OutputKey)) { string output = commandEntity.Output; string rootPath = templateOrigin.Root.Path; string outputDirectory = fileSystem.GetDirectory(output, rootPath, false).FullName; if (outputDirectory.StartsWith(rootPath, StringComparison.Ordinal) && outputDirectory != rootPath) { string partialPath = outputDirectory.Substring(rootPath.Length + 1).CleanPath(); Match match = relativeDirectoryRegex.Match(partialPath); if (match.Success && match.Groups["split"].Success) { basePath = match.Groups["split"].Value; } } } string result = resolver.Resolve(formatOrigin.Name, templateOrigin); if (!string.IsNullOrEmpty(basePath)) { if (!basePath.EndsWith("/", StringComparison.Ordinal)) { basePath += "/"; } result = basePath + result; } return(owner.Create(key, result)); IEntityBase GetTemplateOrigin(out IEntityBase realOrigin) { realOrigin = TemplateEntity.Decorate(owner).FormatOrigin; IEntityBase templateParent = realOrigin; while (templateParent != null && templateParent.Type != EntityKeys.TemplateKey) { templateParent = templateParent.Owner; } return(templateParent?.Owner ?? realOrigin); } } }