Use monobehaviour from asmdef generated assembly - c#
We would like to distribute our project with assembly files instead of .cs scripts.
We thought that this would be easy thanks to assembly definition files, as unity is creating assembly files for the scripts they refer to anyway.
It turns out that when removing the .cs files and putting the assemblies, we ran into a problem :
The monobehaviors defined in the assemblies (so previously in our scripts) can't be added manually to a scene :
"Can't add script component xxx because the script class cannot be found"
While if we add the component through script (i.e. AddComponent) it works.
I'm using Unity 2017.3.f1 to generate the assembly files
Is there a trick to make this work ? or should I try to generate the assemblies using another approach ?
OP here.
Short answer is : don't keep both asmdef and assembly files. Remove the asmdef file if you replace the scripts with the generated assembly
What I ended up doing is the roughly following (this was for CI purpose):
First, we need to make sure Unity compiles the assembly file. So I have a GenerateAssemblies.cs file in an Editor folder that can be executed from command line:
GenerateAssemblies.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEngine;
[InitializeOnLoad]
public static class GenerateAssemblies
{
private static string BATCH_MODE_PARAM = "-batchmode";
private const string REPLACE_ASSEMBLY_PARAM = "-replaceassembly";
static GenerateAssemblies()
{
List<String> args = Environment.GetCommandLineArgs().ToList();
if (args.Any(arg => arg.ToLower().Equals(BATCH_MODE_PARAM)))
{
Debug.LogFormat("GenerateAssemblies will try to parse the command line to replace assemblies.\n" +
"\t Use {0} \"assemblyname\" for every assembly you wish to replace"
, REPLACE_ASSEMBLY_PARAM);
}
if (args.Any(arg => arg.ToLower().Equals(REPLACE_ASSEMBLY_PARAM))) // is a replacement requested ?
{
int lastIndex = 0;
while (lastIndex != -1)
{
lastIndex = args.FindIndex(lastIndex, arg => arg.ToLower().Equals(REPLACE_ASSEMBLY_PARAM));
if (lastIndex >= 0 && lastIndex + 1 < args.Count)
{
string assemblyToReplace = args[lastIndex + 1];
if (!assemblyToReplace.EndsWith(ReplaceAssemblies.ASSEMBLY_EXTENSION))
assemblyToReplace = assemblyToReplace + ReplaceAssemblies.ASSEMBLY_EXTENSION;
ReplaceAssemblies.instance.AddAssemblyFileToReplace(assemblyToReplace);
Debug.LogFormat("Added assembly {0} to the list of assemblies to replace.", assemblyToReplace);
lastIndex++;
}
}
CompilationPipeline.assemblyCompilationFinished += ReplaceAssemblies.instance.ReplaceAssembly; /* This serves as callback after Unity as compiled an assembly */
Debug.Log("Forcing recompilation of all scripts");
// to force recompilation
PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.Standalone, PlayerSettings.GetScriptingDefineSymbolsForGroup(BuildTargetGroup.Standalone) + ";DUMMY_SYMBOL");
AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
}
}
}
Then I have a ReplaceAssemblies.cs file in an editor folder that will :
find the assembly file correpsonding to the asmdef file
save the guid/classes correspondance of the script files
move the script files in a temporary folder
move the assembly in the same folder as the asmdef file
move the asmdef to a temporary folder
Replace the Guid and File ID values for each script in the assembly (to avoid breaking references in scenes and prefabs)
ReplaceAssemblies.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEngine;
public class ReplaceAssemblies : ScriptableSingleton<ReplaceAssemblies>
{
public static string ASSEMBLY_EXTENSION = ".dll";
public static string ASSEMBLY_DEFINITION_EXTENSION = ".asmdef";
[SerializeField]
private List<String> assembliesFilesToReplace = new List<string>();
[SerializeField]
private List<string> pathsOfAssemblyFilesInAssetFolder = new List<string>();
[SerializeField]
private List<string> pathsOfAssemblyFilesCreatedByUnity = new List<string>();
[SerializeField]
private string tempSourceFilePath;
private static readonly string[] fileListPath = { "*.prefab", "*.unity", "*.asset" };
public string TempSourceFilePath
{
get
{
if (String.IsNullOrEmpty(tempSourceFilePath))
{
tempSourceFilePath = FileUtil.GetUniqueTempPathInProject();
}
return tempSourceFilePath;
}
}
void OnEnable()
{
Debug.Log("temp dir : " + TempSourceFilePath);
}
public void ReplaceAssembly(string assemblyPath, CompilerMessage[] messages)
{
string assemblyFileName = assembliesFilesToReplace.Find(assembly => assemblyPath.EndsWith(assembly));
// is this one of the assemblies we want to replace ?
if (!String.IsNullOrEmpty(assemblyFileName))
{
string[] assemblyDefinitionFilePaths = Directory.GetFiles(".", Path.GetFileNameWithoutExtension(assemblyFileName) + ASSEMBLY_DEFINITION_EXTENSION, SearchOption.AllDirectories);
if (assemblyDefinitionFilePaths.Length > 0)
{
string assemblyDefinitionFilePath = assemblyDefinitionFilePaths[0];
ReplaceAssembly(assemblyDefinitionFilePath);
}
}
}
public void AddAssemblyFileToReplace(string assemblyFile)
{
assembliesFilesToReplace.Add(assemblyFile);
}
private void ReplaceAssembly(string assemblyDefinitionFilePath)
{
Debug.LogFormat("Replacing scripts for assembly definition file {0}", assemblyDefinitionFilePath);
string asmdefDirectory = Path.GetDirectoryName(assemblyDefinitionFilePath);
string assemblyName = Path.GetFileNameWithoutExtension(assemblyDefinitionFilePath);
Assembly assemblyToReplace = CompilationPipeline.GetAssemblies().ToList().Find(assembly => assembly.name.ToLower().Equals(assemblyName.ToLower()));
string assemblyPath = assemblyToReplace.outputPath;
string assemblyFileName = Path.GetFileName(assemblyPath);
string[] assemblyFilePathInAssets = Directory.GetFiles("./Assets", assemblyFileName, SearchOption.AllDirectories);
// save the guid/classname correspondance of the scripts that we will remove
Dictionary<string, string> oldGUIDToClassNameMap = new Dictionary<string, string>();
if (assemblyFilePathInAssets.Length <= 0)
{
// Move all script files outside the asset folder
foreach (string sourceFile in assemblyToReplace.sourceFiles)
{
string tempScriptPath = Path.Combine(TempSourceFilePath, sourceFile);
Directory.CreateDirectory(Path.GetDirectoryName(tempScriptPath));
if (!File.Exists(sourceFile))
Debug.LogErrorFormat("File {0} does not exist while the assembly {1} references it.", sourceFile, assemblyToReplace.name);
Debug.Log("will move " + sourceFile + " to " + tempScriptPath);
// save the guid of the file because we may need to replace it later
MonoScript monoScript = AssetDatabase.LoadAssetAtPath<MonoScript>(sourceFile);
if (monoScript != null && monoScript.GetClass() != null)
oldGUIDToClassNameMap.Add(AssetDatabase.AssetPathToGUID(sourceFile), monoScript.GetClass().FullName);
FileUtil.MoveFileOrDirectory(sourceFile, tempScriptPath);
}
Debug.Log("Map of GUID/Class : \n" + String.Join("\n", oldGUIDToClassNameMap.Select(pair => pair.Key + " : " + pair.Value).ToArray()));
string finalAssemblyPath = Path.Combine(asmdefDirectory, assemblyFileName);
Debug.Log("will move " + assemblyPath + " to " + finalAssemblyPath);
FileUtil.MoveFileOrDirectory(assemblyPath, finalAssemblyPath);
string tempAsmdefPath = Path.Combine(TempSourceFilePath, Path.GetFileName(assemblyDefinitionFilePath));
Debug.Log("will move " + assemblyDefinitionFilePath + " to " + tempAsmdefPath);
FileUtil.MoveFileOrDirectory(assemblyDefinitionFilePath, tempAsmdefPath);
// Rename the asmdef meta file to the dll meta file so that the dll guid stays the same
FileUtil.MoveFileOrDirectory(assemblyDefinitionFilePath + ".meta", finalAssemblyPath + ".meta");
pathsOfAssemblyFilesInAssetFolder.Add(finalAssemblyPath);
pathsOfAssemblyFilesCreatedByUnity.Add(assemblyPath);
// We need to refresh before accessing the assets in the new assembly
AssetDatabase.Refresh();
// We need to remove .\ when using LoadAsslAssetsAtPath
string cleanFinalAssemblyPath = finalAssemblyPath.Replace(".\\", "");
var assetsInAssembly = AssetDatabase.LoadAllAssetsAtPath(cleanFinalAssemblyPath);
// list all components in the assembly file.
var assemblyObjects = assetsInAssembly.OfType<MonoScript>().ToArray();
// save the new GUID and file ID for the MonoScript in the new assembly
Dictionary<string, KeyValuePair<string, long>> newMonoScriptToIDsMap = new Dictionary<string, KeyValuePair<string, long>>();
// for each component, replace the guid and fileID file
for (var i = 0; i < assemblyObjects.Length; i++)
{
long dllFileId;
string dllGuid = null;
if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(assemblyObjects[i], out dllGuid, out dllFileId))
{
string fullClassName = assemblyObjects[i].GetClass().FullName;
newMonoScriptToIDsMap.Add(fullClassName, new KeyValuePair<string, long>(dllGuid, dllFileId));
}
}
Debug.Log("Map of Class/GUID:FILEID : \n" + String.Join("\n", newMonoScriptToIDsMap.Select(pair => pair.Key + " : " + pair.Value.Key + " - " + pair.Value.Value).ToArray()));
ReplaceIdsInAssets(oldGUIDToClassNameMap, newMonoScriptToIDsMap);
}
else
{
Debug.Log("Already found an assembly file named " + assemblyFileName + " in asset folder");
}
}
/// <summary>
/// Replace ids in all asset files using the given maps
/// </summary>
/// <param name="oldGUIDToClassNameMap">Maps GUID to be replaced => FullClassName</param>
/// <param name="newMonoScriptToIDsMap">Maps FullClassName => new GUID, new FileID</param>
private static void ReplaceIdsInAssets(Dictionary<string, string> oldGUIDToClassNameMap, Dictionary<string, KeyValuePair<string, long>> newMonoScriptToIDsMap)
{
StringBuilder output = new StringBuilder("Report of replaced ids : \n");
// list all the potential files that might need guid and fileID update
List<string> fileList = new List<string>();
foreach (string extension in fileListPath)
{
fileList.AddRange(Directory.GetFiles(Application.dataPath, extension, SearchOption.AllDirectories));
}
foreach (string file in fileList)
{
string[] fileLines = File.ReadAllLines(file);
for (int line = 0; line < fileLines.Length; line++)
{
//find all instances of the string "guid: " and grab the next 32 characters as the old GUID
if (fileLines[line].Contains("guid: "))
{
int index = fileLines[line].IndexOf("guid: ") + 6;
string oldGUID = fileLines[line].Substring(index, 32); // GUID has 32 characters.
if (oldGUIDToClassNameMap.ContainsKey(oldGUID) && newMonoScriptToIDsMap.ContainsKey(oldGUIDToClassNameMap[oldGUID]))
{
fileLines[line] = fileLines[line].Replace(oldGUID, newMonoScriptToIDsMap[oldGUIDToClassNameMap[oldGUID]].Key);
output.AppendFormat("File {0} : Found GUID {1} of class {2}. Replaced with new GUID {3}.", file, oldGUID, oldGUIDToClassNameMap[oldGUID], newMonoScriptToIDsMap[oldGUIDToClassNameMap[oldGUID]].Key);
if (fileLines[line].Contains("fileID: "))
{
index = fileLines[line].IndexOf("fileID: ") + 8;
int index2 = fileLines[line].IndexOf(",", index);
string oldFileID = fileLines[line].Substring(index, index2 - index); // GUID has 32 characters.
fileLines[line] = fileLines[line].Replace(oldFileID, newMonoScriptToIDsMap[oldGUIDToClassNameMap[oldGUID]].Value.ToString());
output.AppendFormat("Replaced fileID {0} with {1}", oldGUID, newMonoScriptToIDsMap[oldGUIDToClassNameMap[oldGUID]].Value.ToString());
}
output.Append("\n");
}
}
}
//Write the lines back to the file
File.WriteAllLines(file, fileLines);
}
Debug.Log(output.ToString());
}
[MenuItem("Tools/Replace Assembly")]
public static void ReplaceAssemblyMenu()
{
string assemblyDefinitionFilePath = EditorUtility.OpenFilePanel(
title: "Select Assembly Definition File",
directory: Application.dataPath,
extension: ASSEMBLY_DEFINITION_EXTENSION.Substring(1));
if (assemblyDefinitionFilePath.Length == 0)
return;
instance.ReplaceAssembly(assemblyDefinitionFilePath);
}
}
I was experiencing this issue, and like you, I was using the information provided from asmdef files to provide all the required information (which .cs files, what references, defines, etc) to build an assembly.
I found that the issue was the DLL I was creating had the same name as the asmdef file I was using to provide the information. Even though the asmdef file was no longer being compiled (because all the scripts had been removed to build the DLL), it was still interfering with the project.
So for me, the inconsistency between accessing a script from inside the editor and from inside scripts was because there was a DLL and as asmdef file with the same name in the project.
Giving the compiled DLL a different name or removing the asmdef file was the solution for me.
Just tested with Unity 2019.3.0b1.
Content of test class:
using System.Reflection;
using UnityEngine;
namespace Assets.Test
{
public class TestBehaviour : MonoBehaviour
{
void Start()
{
Debug.Log(Assembly.GetAssembly(GetType()));
}
}
}
First project with source code and assembly definition file
Second project with the generated DLL, working as expected
As far as i'm concerned, the use of asmdef merely forces unity3d to compile your scripts into separate assemblies that are then referenced by your project.
Actually, it creates projects in your unity solution that contain your .cs files and each of these projects is compiled into its own output assembly.
The error you are seeing might be related to assembly caching.
I've had that error a few months ago and it was due to an outdated assembly still being cached.
As a result, unity3d editor kinda hiccuped when loading the project and therefore could not load the specific assembly.
I fixed it by deleting the directories Library, obj and Temp and then reloaded the unity3d project.
To get rid of that for good, we have moved away from asmdef and .cs files inside our unity projects once and for all.
All our scripts have been extracted to separate projects that are never touched by unity3d.
Every project references UnityEngine.dll and/or UnityEditor.dll (for Editor assemblies) depending on which unity3d types it may require.
The projects are built locally using Visual Studio or server side in our CI pipeline.
Output is copied manually into the assets directory of a unity project where it is then loaded from within unity3d editor.
This last step is a pain admittedly but i have yet to find time to streamline this process some more.
This approach has a few benefits
We are in control of our code in one single repository.
There is only one single point of truth and every developer commits changes onto the same code base.
There are no copies of .cs files across any number of unity projects that consume our types.
There is no need to figure out merge conflicts from updating a unitypackage where there have been deletions.
Unit tests can be done server side (CI pipeline) without the need of some docker image with unity3d on top (ofc there are restrictions on how much you can test without the entire unity3d environment running).
We create our own NuGet packages that can be referenced in projects (vcproj, not unity projects!).
Types deriving from MonoBehaviour can be added to GameObjects via code or via unity3d editor.
You also get to explore loaded assemblies inside your unity3d editor project view by clicking on the arrow of an assembly which will expand to show the list of contained relevant types.
Let's talk about downsides
One example is that we use SteamVR for interacting with controls.
The SteamVR plugin for unity3d is released through unity's asset store and annoyingly it contains script files and resources but no assemblies.
This goes for pretty much all assets in the store by the way.
Since we can't build against code, we have to go through the trouble of compiling SteamVR once and then copy the output assembly somewhere else.
This is not just as tedious as a task can be, it also has some limitations of its own which i get to later.
Anyway, this lets us reference a compiled asset with our own code so we get to use asset specific types like SteamVR_Action in our code without having to use unity3d editor and script files in unity projects (or reflection which would be even worse).
Limitations of compiled assets like this are two fold.
For once, it is horribly inefficient to get there in the first place.
On the other hand, you'll only have to do that once for every version of an asset.
Once that's done, make it a private NuGet package and you're golden.
The other limitation is the way how unity3d approaches dependency injection.
Actually i'm not entirely sure what it really is they try to do but here goes.
Unity3d wants you to only ever reference assemblies from within ../UnityInstallDirectory/Editor/Data/Managed/.
In a perfect world, your own assemblies reference that big gunky UnityEngine.dll in this directory and once loaded by unity3d editor everything works as expected.
When you compile a unity project from within unity3d editor however, the resulting assembly references all the assemblies from within ../UnityInstallDirectory/Editor/Data/Managed/UnityEngine/ which contains a very small version of UnityEngine.dll which in turn acts as a type forwarder to all the other sub modules.
Not such a perfect world now is it?
Your previously compiled asset requires the type MonoBehaviour to sit in an assembly called UnityEngine.CoreModule.dll.
Your own project however expects it to sit in UnityEngine.dll since you're a good fellow and follow the rules.
This is just asking for trouble and to get around this problem we are now directly referencing all the managed sub modules from within ../UnityInstallDirectory/Editor/Data/Managed/UnityEngine/.
We also ignore unity3d editor moaning about how we are doing it wrong.
tl;dr
By doing all from above and leaving asmdef and .cs files out of the equation we are able to build, unit test and pack our logic and types into assemblies.
We are also able to keep a clean code base that can be easily maintained and extended without dozens of copies of the same code in multiple locations and/or repositories.
Why unity3d does things the way they do, i'll never understand.
I do know there is a thing called Building from HEAD but since the entirety of the .net ecosystem is using the binary format to share content in the form of referable assemblies, why would you want to do things differently?
This is a topic for another day though.
If you made it all the way through this post, i sincerely hope it is helping you fix your problem at hand.
In case i misinterpreted your question ... sorry :-)
Unity3d is weird ...
Related
"The name "Asset Database" does not exist in the current context" error while building unity game [duplicate]
This question already has answers here: UNITY - The name `AssetDatabase' does not exist in the current context (3 answers) Closed 2 years ago. My teacher gave me a challenge in unity (c#) where I have to load prefabs into an array (as a GameObject[] function). The code I have works in the editor, but when I build the game, it gives me the error The name "Asset Database" does not exist in the curernt context. The code I have currently is: using System.Collections.Generic; using System.IO; using UnityEngine; using UnityEditor; using System; public class FindCards : MonoBehaviour { public string directoryName; public List<GameObject> foundGameObjects; public GameObject[] ListOfCards() { // get the directory and the files in them DirectoryInfo dirInfo = new DirectoryInfo(Application.dataPath + directoryName); FileInfo[] fileInfo = dirInfo.GetFiles("*.prefab"); // loop through the files foreach(FileInfo file in fileInfo) { // get the path to the directory again (with proper formatting) string fullPath = file.FullName.Replace(#"\","/"); string assetPath = "Assets" + fullPath.Replace(Application.dataPath, ""); GameObject _asset; // load the asset from the direcotry _asset = AssetDatabase.LoadAssetAtPath(assetPath, typeof(GameObject)) as GameObject; // add it to the list foundGameObjects.Add(_asset); Debug.Log(_asset.name); } // return the list as an array return(foundGameObjects.ToArray()); } public void SetCardProperties(string cardName, Card card) { // get the first string, and the int // from the card's name string[] sub = cardName.Split('_'); // (if the name was Diamond_07, // string name // there would be an int = 07 and // name[0] // a string = Diamond) // name[1] card.suit = sub[0]; Int32.TryParse(sub[1], out card.number); } } The code that is calling this function looks like this (note that this is in a different script): private void Start() { canScore = true; gameIsGoing = true; // set up cards cards = findCards.ListOfCards(); for (int i = 0; i < cards.Length; i++) { Card card = cards[i].GetComponent<Card>(); findCards.SetCardProperties(cards[i].name, card); } // set up deck ResetDeck(); } The directory here: Prefabs in the directory Errors here: Errors unity gives me I have been pulling my hair out, because there was nothing online I could find that would help me. My teacher gave this to me as challenge because he also did not know how to do it which is why I'm asking this question... Thanks in advance!
AssetsDatabase it seems that works only in Editor. Check this thread for more info: https://answers.unity.com/questions/1068055/assetdatabase.html Edit: `Resources.Load()ยด search in the path: "Assets/Resources". Look what documentation says: If an asset can be found at path, it is returned with type T, otherwise returns null. If the file at path is of a type that cannot be converted to T, also returns null. The path is relative to any folder named Resources inside the Assets folder of your project. More than one Resources folder can be used. For example, a project may have Resources folders called Assets/Resources and ##AssetsAssetsResources##. The path does not need to include Assets and Resources in the string, for example loading a GameObject at ##AssetspathShotgun.prefab## would only require Shotgun as the path. Also, if ##AssetspathMissiles/PlasmaGun.prefab exists it can be loaded using GunspathPlasmaGun## as the path string. If you have multiple Resources folders you cannot duplicate the use of an asset name.
How to use a common method across all projects without having to pass the namespaces
I am working on localization of texts across all the 5 projects which together forms the product. Localization: So If user is from USA, they will see the product in en-US, if they are from China they will see the text in ch-CH. And I am stuck at below stuff. Each project will have its OWN bucket of Resx file (file where I am keeping for translations). Project A - en-US.resx file cn-CH.resx file Project B - en-US.resx file ch-CH.resx file Project C - en-US.resx file ch-CH.resx file . . . Now I have a Project Common which gets referenced by all the projects. So What I wrote a singleton class in Common public sealed class Translation { private static readonly Translation translation = new Translation(); public static Translation GetTranslation { get { return translation; } } private Translation() { } static Translation() { } public string GetTranslatedMessage(string key, CultureInfo culture, string message, string namespace) { var rm = new ResourceManager("namespace", Assembly.GetExecutingAssembly()); message = rm.GetString(key, culture); return message; } } So far so good, As you can see I am using namespace as 4th parameter with resource manager so that I can look for the translation in the right project bucket , I just do something like below: Translation.Translate(key, culture, message, namespace) // singleton class taking in the namespace to find the right bucket And it works fine. Question/Problem: But from every project I need to pass the namespace, I mean where ever I call I need to pass the namespace. I am wondering is there any way, I can implicitly tell which bucket each project needs to look into. Can I use Abstract or 2 singleton classes, factory may be?, or something like that. I am newbie so I am not familiar on how to tackle this issue. I just don't want to pass namespace in every call. WorkAround: I can repeat this same singleton code in each project and get the stuff working, but then I will be repeating same singleton code in each project/
If you are open to a hack-y solution, and your various namespaced files are in folders named after the namespace, you could use the CallerFilePathAttribute and split the namespace out of the path. It does seem fragile: Look up C# Caller Information. The sample they show is: public void DoProcessing() { TraceMessage("Something happened."); } public void TraceMessage(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0) { System.Diagnostics.Trace.WriteLine("message: " + message); System.Diagnostics.Trace.WriteLine("member name: " + memberName); System.Diagnostics.Trace.WriteLine("source file path: " + sourceFilePath); System.Diagnostics.Trace.WriteLine("source line number: " + sourceLineNumber); } // Sample Output: // message: Something happened. // member name: DoProcessing // source file path: c:\Users\username\Documents\Visual Studio 2012\Projects\CallerInfoCS\CallerInfoCS\Form1.cs // source line number: 31 Then take what they have labeled as source file path and play path games to get the namespace. Parameters using these attributes are always optional (typically with they defaults set to default(T)). The compiler injects the value, the caller should not.
Manipulating files block them
I'm writing a WinForms program that uses MEF to load assemblies. Those assemblies are not located in the same folder than the executable. As I need to perform some file maintenance, I implemented some code in the file Program.cs, before loading the actual WinForm, so the files (even if assemblies) are not loaded (or shouldn't if they are) by the program. I'm performing two operations: - Moving a folder from one location to an other one - Unzipping files from an archive and overwrite dll files from the folder moved (if file from the archive is newer than the one moved) The problem is that after moving the folder, files in it are locked and cannot be overwritten. I also tried to move files one by one by disposing them when the move is finished. Can someone explain me why the files are blocked and how I could avoid that Thanks private static void InitializePluginsFolder() { if (!Directory.Exists(Paths.PluginsPath)) { Directory.CreateDirectory(Paths.PluginsPath); } // Find archive that contains plugins to deploy var assembly = Assembly.GetExecutingAssembly(); if (assembly.Location == null) { throw new NullReferenceException("Executing assembly is null!"); } var currentDirectory = new FileInfo(assembly.Location).DirectoryName; if (currentDirectory == null) { throw new NullReferenceException("Current folder is null!"); } // Check if previous installation contains a "Plugins" folder var currentPluginsPath = Path.Combine(currentDirectory, "Plugins"); if (Directory.Exists(currentPluginsPath)) { foreach (FileInfo fi in new DirectoryInfo(currentPluginsPath).GetFiles()) { using (FileStream sourceStream = new FileStream(fi.FullName, FileMode.Open)) { using (FileStream destStream = new FileStream(Path.Combine(Paths.PluginsPath, fi.Name), FileMode.Create)) { destStream.Lock(0, sourceStream.Length); sourceStream.CopyTo(destStream); } } } Directory.Delete(currentPluginsPath, true); } // Then updates plugins with latest version of plugins (zipped) var pluginsZipFilePath = Path.Combine(currentDirectory, "Plugins.zip"); // Extract content of plugins archive to a temporary folder var tempPath = string.Format("{0}_Temp", Paths.PluginsPath); if (Directory.Exists(tempPath)) { Directory.Delete(tempPath, true); } ZipFile.ExtractToDirectory(pluginsZipFilePath, tempPath); // Moves all plugins to appropriate folder if version is greater // to the version in place foreach (var fi in new DirectoryInfo(tempPath).GetFiles()) { if (fi.Extension.ToLower() != ".dll") { continue; } var targetFile = Path.Combine(Paths.PluginsPath, fi.Name); if (File.Exists(targetFile)) { if (fi.GetAssemblyVersion() > new FileInfo(targetFile).GetAssemblyVersion()) { // If version to deploy is newer than current version // Delete current version and copy the new one // FAILS HERE File.Copy(fi.FullName, targetFile, true); } } else { File.Move(fi.FullName, targetFile); } } // Delete temporary folder Directory.Delete(tempPath, true); }
Check the implementation of the GetAssemblyVersion() method used in this part of code: if (File.Exists(targetFile)) { if (fi.GetAssemblyVersion() > new FileInfo(targetFile).GetAssemblyVersion()) { // If version to deploy is newer than current version // Delete current version and copy the new one // FAILS HERE File.Copy(fi.FullName, targetFile, true); } } fi variable has type FileInfo, GetAssemblyVersion() looks like an extension method. You should check how assembly version is retrieved from the file. If this method loads an assembly it should also unload it to release the file. The separate AppDomain is helpful if you need to load the assembly, do the job and after that unload it. Here is the GetAssemblyVersion method implementation: public static Version GetAssemblyVersion(this FileInfo fi) { AppDomain checkFileDomain = AppDomain.CreateDomain("DomainToCheckFileVersion"); Assembly assembly = checkFileDomain.Load(new AssemblyName {CodeBase = fi.FullName}); Version fileVersion = assembly.GetName().Version; AppDomain.Unload(checkFileDomain); return fileVersion; } The following implementation of the GetAssemblyVersion() could retrieve the assembly version without loading assembly into your AppDomain. Thnx #usterdev for the hint. It also allows you to get the version without assembly references resolve: public static Version GetAssemblyVersion(this FileInfo fi) { return AssemblyName.GetAssemblyName(fi.FullName).Version; }
You have to make sure that you are not loading the Assembly into your domain to get the Version from it, otherwise the file gets locked. By using the AssemblyName.GetAssemblyName() static method (see MSDN), the assembly file is loaded, version is read and then unloaded but not added to your domain. Here an extension for FileInfo doing so: public static Version GetAssemblyVersion(this FileInfo fi) { AssemblyName an = AssemblyName.GetAssemblyName(fi.FullName); return an.Version; }
The below statement locks the file destStream.Lock(0, sourceStream.Length); but after that you havent unlocked the file. Perhaps that is the cause of your problem.
I would start checking if you program has actually already loaded the assembly. two suggestions: 1 - Call a method like this before calling your InitializePluginsFolder static void DumpLoadedAssemblies() { var ads = AppDomain.CurrentDomain.GetAssemblies(); Console.WriteLine(ads.Length); foreach (var ad in ads) { Console.WriteLine(ad.FullName); // maybe this can be helpful as well foreach (var f in ad.GetFiles()) Console.WriteLine(f.Name); Console.WriteLine("*******"); } } 2 - In the first line of Main, register for AssemblyLoad Event and dump Loaded Assembly in the event handler public static void Main() { AppDomain.CurrentDomain.AssemblyLoad += OnAssemlyLoad; ... } static void OnAssemlyLoad(object sender, AssemblyLoadEventArgs args) { Console.WriteLine("Assembly Loaded: " + args.LoadedAssembly.FullName); }
You definitely load assembly using AssemblyName.GetAssemblyName, unfortunately .NET has no conventional ways of checking assembly metadata without loading assembly. To avoid this you can: Load assembly in separated AppDomain as Nikita suggested, I can add: load it with ReflectionOnlyLoad Or get assembly version using Mono.Cecil library as Reflector does Just for completeness: actually you can load assembly into same AppDomain without locking assembly file in two stage: read file contents into byte[] and using Assembly.Load(byte[] rawAssembly) but this way has serious "Loading Context" issues and what will you do with several loaded assemblies :)
Get NameSpace of my Test Class
I have a webdriver solution that has 10 or so projects in it. 1 core assembly/dll that holds all the common methods and 9 other test assemblies that use those methods in their tests. I need to access an embedded resource for one of those 9 assemblies but I need to do it from inside the core dll. What's the best way to do that. namespace = webdriver.core json.cs - reads a json file and returns it as a string namespace = webdriver.marketplacestest marketplace1Test.cs - calls one of the methods in json.cs such as getName(); profile.json - holds {"marketplace1" : "Amazon"} calling an embedded resource from a known namespace is easy. I did that like this: private static string fromEmbeddedResource(string myNamespace, string myFolder, string fileName) { string result; using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(myNamespace + "." + myFolder + "." + fileName)) using (StreamReader reader = new StreamReader(stream)) { result = reader.ReadToEnd(); } return result; } As you can see, I just call the following and I have the file as a string string json = fromEmbeddedResource("WebDriver.Core", "CFG", "pid.json"); It's harder though when the file is embedded in one of my test dlls. Anyone know how I can access or get the assembly's namespace? I tried... Assembly.GetCallingAssembly().GetTypes(); but it looks like it's pulling types from the WebDriver.Core.dll assembly and not the WebDriver.Marketplace1.dll assembly.
I was able to figure it out. The problem I had was the calling assembly wasn't the correct assembly because I was calling a method in my core dll that called another method in my core dll. I got it working by passing the assembly along but that was expensive. To make things more efficient I modified my static SettingsRepository class that holds the two assemblies in a dictionary. That way I can pass in a string of "core" or "test" and pull the assembly without having to determine if I'm using an executing assembly or calling assembly each time. private static Dictionary<string, object> _assembly = new Dictionary<string,object>(); public static Assembly getAssembly (string type) { return _assembly[type] as Assembly; } public static void addAssembly(string myType, Assembly assembly) { bool containsKey = _assembly.ContainsKey(myType); if (!containsKey) { _assembly.Add(myType, assembly); } } When I start a test, I always initialize my driver class first so i added the following two sets to that constructor. Settings.addAssembly("core", Assembly.GetExecutingAssembly()); Settings.addAssembly("test", Assembly.GetCallingAssembly()); Now I can call either assembly I want anytime I want and know which one I'm getting.
How do you get the current project directory from C# code when creating a custom MSBuild task?
Instead of running an external program with its path hardcoded, I would like to get the current Project Dir. I'm calling an external program using a process in the custom task. How would I do that? AppDomain.CurrentDomain.BaseDirectory just gives me the location of VS 2008.
using System; using System.IO; // This will get the current WORKING directory (i.e. \bin\Debug) string workingDirectory = Environment.CurrentDirectory; // or: Directory.GetCurrentDirectory() gives the same result // This will get the current PROJECT bin directory (ie ../bin/) string projectDirectory = Directory.GetParent(workingDirectory).Parent.FullName; // This will get the current PROJECT directory string projectDirectory = Directory.GetParent(workingDirectory).Parent.Parent.FullName;
You can try one of this two methods. string startupPath = System.IO.Directory.GetCurrentDirectory(); string startupPath = Environment.CurrentDirectory; Tell me, which one seems to you better
If a project is running on an IIS express, the Environment.CurrentDirectory could point to where IIS Express is located ( the default path would be C:\Program Files (x86)\IIS Express ), not to where your project resides. This is probably the most suitable directory path for various kinds of projects. AppDomain.CurrentDomain.BaseDirectory This is the MSDN definition. Gets the base directory that the assembly resolver uses to probe for assemblies.
The proper1 way to get the root folder of a C# project is to leverage the [CallerFilePath] attribute to obtain the full path name of a source file, and then subtract the filename plus extension from it, leaving you with the path to the project. Here is how to actually do it: In the root folder of your project, add file ProjectSourcePath.cs with the following content: internal static class ProjectSourcePath { private const string myRelativePath = nameof(ProjectSourcePath) + ".cs"; private static string? lazyValue; public static string Value => lazyValue ??= calculatePath(); private static string calculatePath() { string pathName = GetSourceFilePathName(); Assert( pathName.EndsWith( myRelativePath, StringComparison.Ordinal ) ); return pathName.Substring( 0, pathName.Length - myRelativePath.Length ); } } The string? requires a pretty late version of C# with #nullable enable; if you don't have it, then just remove the ?. The Assert() function is my own; you can replace it with your own, or omit it, if you like living your life dangerously. The function GetSourceFilePathName() is defined as follows: using System.Runtime.CompilerServices public static string GetSourceFilePathName( [CallerFilePath] string? callerFilePath = null ) // => callerFilePath ?? ""; Once you have the above, you can use it as follows: string projectSourcePath = ProjectSourcePath.Value; 1 'proper' as in: fool-proof; sure-fire; without presumptions; not being held together by shoestrings; not bound to work for some projects but fail for others; not likely to horribly break without a warning when you change unrelated things; etc.
This will also give you the project directory by navigating two levels up from the current executing directory (this won't return the project directory for every build, but this is the most common). System.IO.Path.GetFullPath(#"..\..\") Of course you would want to contain this inside some sort of validation/error handling logic.
If you want ot know what is the directory where your solution is located, you need to do this: var parent = Directory.GetParent(Directory.GetCurrentDirectory()).Parent; if (parent != null) { var directoryInfo = parent.Parent; string startDirectory = null; if (directoryInfo != null) { startDirectory = directoryInfo.FullName; } if (startDirectory != null) { /*Do whatever you want "startDirectory" variable*/} } If you let only with GetCurrrentDirectory() method, you get the build folder no matter if you are debugging or releasing. I hope this help! If you forget about validations it would be like this: var startDirectory = Directory.GetParent(Directory.GetCurrentDirectory()).Parent.Parent.FullName;
Based on Gucu112's answer, but for .NET Core Console/Window application, it should be: string projectDir = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, #"..\..\..")); I'm using this in a xUnit project for a .NET Core Window Application.
If you really want to ensure you get the source project directory, no matter what the bin output path is set to: Add a pre-build event command line (Visual Studio: Project properties -> Build Events): echo $(MSBuildProjectDirectory) > $(MSBuildProjectDirectory)\Resources\ProjectDirectory.txt Add the ProjectDirectory.txt file to the Resources.resx of the project (If it doesn't exist yet, right click project -> Add new item -> Resources file) Access from code with Resources.ProjectDirectory.
This solution works well for me, on Develop and also on TEST and PROD servers with ASP.NET MVC5 via C#: var projectDir = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory); If you need project directory in project configuration file use: $(ProjectDir)
I was looking for this too. I've got a project that runs HWC, and I'd like to keep the web site out of the app tree, but I don't want to keep it in the debug (or release) directory. FWIW, the accepted solution (and this one as well) only identifies the directory the executable is running in. To find that directory, I've been using string startupPath = System.IO.Path.GetFullPath(".\\").
using System; using System.IO; // Get the current directory and make it a DirectoryInfo object. // Do not use Environment.CurrentDirectory, vistual studio // and visual studio code will return different result: // Visual studio will return #"projectDir\bin\Release\netcoreapp2.0\", yet // vs code will return #"projectDir\" var currentDirectory = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory); // On windows, the current directory is the compiled binary sits, // so string like #"bin\Release\netcoreapp2.0\" will follow the project directory. // Hense, the project directory is the great grand-father of the current directory. string projectDirectory = currentDirectory.Parent.Parent.Parent.FullName;
I had a similar situation, and after fruitless Googles, I declared a public string, which mods a string value of the debug / release path to get the project path. A benefit of using this method is that since it uses the currect project's directory, it matters not if you are working from a debug directory or a release directory: public string DirProject() { string DirDebug = System.IO.Directory.GetCurrentDirectory(); string DirProject = DirDebug; for (int counter_slash = 0; counter_slash < 4; counter_slash++) { DirProject = DirProject.Substring(0, DirProject.LastIndexOf(#"\")); } return DirProject; } You would then be able to call it whenever you want, using only one line: string MyProjectDir = DirProject(); This should work in most cases.
Another way to do this string startupPath = System.IO.Directory.GetParent(#"./").FullName; If you want to get path to bin folder string startupPath = System.IO.Directory.GetParent(#"../").FullName; Maybe there are better way =)
Yet another imperfect solution (but perhaps a little closer to perfect than some of the others): protected static string GetSolutionFSPath() { return System.IO.Directory.GetParent(System.IO.Directory.GetCurrentDirectory()).Parent.Parent.FullName; } protected static string GetProjectFSPath() { return String.Format("{0}\\{1}", GetSolutionFSPath(), System.Reflection.Assembly.GetExecutingAssembly().GetName().Name); } This version will return the current projects' folder even if the current project is not the Startup Project for the solution. The first flaw with this is that I've skipped all error checking. That can be fixed easy enough but should only be a problem if you're storing your project in the root directory for the drive or using a junction in your path (and that junction is a descendant of the solution folder) so this scenario is unlikely. I'm not entirely sure that Visual Studio could handle either of these setups anyway. Another (more likely) problem that you may run into is that the project name must match the folder name for the project for it to be found. Another problem you may have is that the project must be inside the solution folder. This usually isn't a problem but if you've used the Add Existing Project to Solution option to add the project to the solution then this may not be the way your solution is organized. Lastly, if you're application will be modifying the working directory, you should store this value before you do that because this value is determined relative to the current working directory. Of course, this all also means that you must not alter the default values for your projects' Build->Output path or Debug->Working directory options in the project properties dialog.
Try this, its simple HttpContext.Current.Server.MapPath("~/FolderName/");
string projPath = Path.GetFullPath(#"..\..\..\"); Console.WriteLine(projPath); This consistently works well for me. Give it a go.
After I had finally finished polishing my first answer regarding the us of public strings to derive an answer, it dawned on me that you could probably read a value from the registry to get your desired result. As it turns out, that route was even shorter: First, you must include the Microsoft.Win32 namespace so you can work with the registry: using Microsoft.Win32; // required for reading and / or writing the registry Here is the main code: RegistryKey Projects_Key = Registry.CurrentUser.OpenSubKey(#"SOFTWARE\Microsoft\VisualStudio\9.0", false); string DirProject = (string)Projects_Key.GetValue(#"DefaultNewProjectLocation"); A note on this answer: I am using Visual Studio 2008 Professional Edition. If you are using another version, (i.e. 2003, 2005, 2010; etc.), then you mayt have to modify the 'version' part of the SubKey string (i.e. 8.0, 7.0; etc.). If you use one of my answers, and if it is not too much to ask, then I would like to know which of my methods you used and why. Good luck. dm
Use this to get the Project directory (worked for me): string projectPath = Directory.GetParent(Directory.GetCurrentDirectory()).Parent.FullName;
I have used following solution to get the job done: string projectDir = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, #"..\.."));
Try: var pathRegex = new Regex(#"\\bin(\\x86|\\x64)?\\(Debug|Release)$", RegexOptions.Compiled); var directory = pathRegex.Replace(Directory.GetCurrentDirectory(), String.Empty); This is solution different from the others does also take into account possible x86 or x64 build.
(Because 22 answers are not enough... here's one more....) Mike Nakis posted a great answer, to which I added a few enhancements. This is just a slightly spiffed up version of his very nice code. As Mike pointed out, this class file must be in the root of the project. I did not run into any problems with the below, but perhaps there are nuances I'm not aware of. YMMV. using System.IO; using System.Runtime.CompilerServices; namespace Whatever { internal static class ProjectPathInfo { public static string CSharpClassFileName = nameof(ProjectPathInfo) + ".cs"; public static string CSharpClassPath; public static string ProjectPath; public static string SolutionPath; static ProjectPathInfo() { CSharpClassPath = GetSourceFilePathName(); ProjectPath = Directory.GetParent(CSharpClassPath)!.FullName; SolutionPath = Directory.GetParent(ProjectPath)!.FullName; } private static string GetSourceFilePathName( [CallerFilePath] string? callerFilePath = null ) => callerFilePath ?? ""; } }
Ok, 2021, a bit late to the party... but very annoyed by all possibilities I found in many projects: bin/Debug bin/x86/Debug bin/Debug/net5.0-windows ... Come on... I just need a one-liner (or almost) to address some files in test units; I need to use it on all past, current, (maybe future) projects. So, if the project name is the same of relative folder which it lies in: use the assembly name to pick project root folder name; go back until that name is found. Code sample: string appName = Assembly.GetExecutingAssembly().GetName().Name; var dir = new DirectoryInfo(Environment.CurrentDirectory); while (dir.Name != appName) { dir = Directory.GetParent(dir.FullName); } return dir.FullName;
The best solution string PjFolder1 = Directory.GetParent(AppDomain.CurrentDomain.BaseDirectory). Parent.Parent.FullName; Other solution string pjFolder2 = Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName( System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase))); Test it, AppDomain.CurrentDomain.BaseDirectory worked for me on past project, now I get debug folder .... the selected GOOD answer just NOT WORK!. //Project DEBUG folder, but STILL PROJECT FOLDER string pjDebugFolder = AppDomain.CurrentDomain.BaseDirectory; //Visual studio folder, NOT PROJECT FOLDER //This solutions just not work string vsFolder = Directory.GetCurrentDirectory(); string vsFolder2 = Environment.CurrentDirectory; string vsFolder3 = Path.GetFullPath(".\\"); //Current PROJECT FOLDER string ProjectFolder = //Get Debug Folder object from BaseDirectory ( the same with end slash) Directory.GetParent(pjDebugFolder). Parent.//Bin Folder object Parent. //Project Folder object FullName;//Project Folder complete path
This works on VS2017 w/ SDK Core MSBuild configurations. You need to NuGet in the EnvDTE / EnvDTE80 packages. Do not use COM or interop. anything.... garbage!! internal class Program { private static readonly DTE2 _dte2; // Static Constructor static Program() { _dte2 = (DTE2)Marshal.GetActiveObject("VisualStudio.DTE.15.0"); } private static void FindProjectsIn(ProjectItem item, List<Project> results) { if (item.Object is Project) { var proj = (Project) item.Object; if (new Guid(proj.Kind) != new Guid(Constants.vsProjectItemKindPhysicalFolder)) results.Add((Project) item.Object); else foreach (ProjectItem innerItem in proj.ProjectItems) FindProjectsIn(innerItem, results); } if (item.ProjectItems != null) foreach (ProjectItem innerItem in item.ProjectItems) FindProjectsIn(innerItem, results); } private static void FindProjectsIn(UIHierarchyItem item, List<Project> results) { if (item.Object is Project) { var proj = (Project) item.Object; if (new Guid(proj.Kind) != new Guid(Constants.vsProjectItemKindPhysicalFolder)) results.Add((Project) item.Object); else foreach (ProjectItem innerItem in proj.ProjectItems) FindProjectsIn(innerItem, results); } foreach (UIHierarchyItem innerItem in item.UIHierarchyItems) FindProjectsIn(innerItem, results); } private static IEnumerable<Project> GetEnvDTEProjectsInSolution() { var ret = new List<Project>(); var hierarchy = _dte2.ToolWindows.SolutionExplorer; foreach (UIHierarchyItem innerItem in hierarchy.UIHierarchyItems) FindProjectsIn(innerItem, ret); return ret; } private static void Main() { var projects = GetEnvDTEProjectsInSolution(); var solutiondir = Path.GetDirectoryName(_dte2.Solution.FullName); // TODO ... var project = projects.FirstOrDefault(p => p.Name == <current project>); Console.WriteLine(project.FullName); } }
I didn't see a solution by using string.Join and string.Split + SkipLast 4 elements, so here it is. string projectDir = string.Join('/', AppDomain.CurrentDomain.BaseDirectory .Split(new char[] { '/' }) .SkipLast(4));
/* /home/freephoenix888/Programming/csharpProject/bin/Debug/net7.0/csharpProject */ Console.WriteLine(Environment.CurrentDirectory); /* /home/freephoenix888/Programming/csharpProject/ */ Console.WriteLine(Directory.GetParent(Environment.CurrentDirectory).Parent.Parent.Parent.FullName);
Try: { OpenFileDialog fd = new OpenFileDialog(); fd.Multiselect = false; fd.Filter = "Image files (*.bmp, *.jpg)|*.bmp;*.jpg|All files (*.*)|*.*"; if (fd.ShowDialog() == true) { if (fd.CheckFileExists) { var fileNameToSave = GetTimestamp(DateTime.Now) + Path.GetExtension(fd.FileName); var pathRegex = new Regex(#"\\bin(\\x86|\\x64)?\\(Debug|Release)$", RegexOptions.Compiled); var directory = pathRegex.Replace(Directory.GetCurrentDirectory(), String.Empty); var imagePath = Path.Combine(directory + #"\Uploads\" + fileNameToSave); File.Copy(fd.FileName, imagePath); } } } catch (Exception ex) { throw ex; } this is the code for uploading image into wpf upload directory
Directory.GetParent(Directory.GetCurrentDirectory()).Parent.Parent.Parent.Parent.FullName Will give you the project directory.