diff --git a/src/Microsoft.Android.Build.BaseTasks/AndroidAsyncTask.cs b/src/Microsoft.Android.Build.BaseTasks/AndroidAsyncTask.cs
index f3859e50..a04d86ff 100644
--- a/src/Microsoft.Android.Build.BaseTasks/AndroidAsyncTask.cs
+++ b/src/Microsoft.Android.Build.BaseTasks/AndroidAsyncTask.cs
@@ -46,5 +46,7 @@ public virtual bool RunTask ()
 		/// * RunTaskAsync is already on a background thread
 		/// </summary>
 		public virtual System.Threading.Tasks.Task RunTaskAsync () => System.Threading.Tasks.Task.CompletedTask;
+
+		protected object ProjectSpecificTaskObjectKey (object key) => (key, WorkingDirectory);
 	}
 }
diff --git a/src/Microsoft.Android.Build.BaseTasks/AndroidTask.cs b/src/Microsoft.Android.Build.BaseTasks/AndroidTask.cs
index 4b511c3c..9acb7b8b 100644
--- a/src/Microsoft.Android.Build.BaseTasks/AndroidTask.cs
+++ b/src/Microsoft.Android.Build.BaseTasks/AndroidTask.cs
@@ -1,6 +1,7 @@
 // https://github.com/xamarin/xamarin-android/blob/9fca138604c53989e1cff7fc0c2e939583b4da28/src/Xamarin.Android.Build.Tasks/Tasks/AndroidTask.cs#L10
 
 using System;
+using System.IO;
 using Microsoft.Build.Utilities;
 
 namespace Microsoft.Android.Build.Tasks
@@ -11,6 +12,13 @@ public abstract class AndroidTask : Task
 	{
 		public abstract string TaskPrefix { get; }
 
+		protected string WorkingDirectory { get; private set; }
+
+		public AndroidTask ()
+		{
+			WorkingDirectory = Directory.GetCurrentDirectory();
+		}
+
 		public override bool Execute ()
 		{
 			try {
@@ -22,5 +30,7 @@ public override bool Execute ()
 		}
 
 		public abstract bool RunTask ();
+
+		protected object ProjectSpecificTaskObjectKey (object key) => (key, WorkingDirectory);
 	}
 }
diff --git a/src/Microsoft.Android.Build.BaseTasks/AndroidToolTask.cs b/src/Microsoft.Android.Build.BaseTasks/AndroidToolTask.cs
index 38093a61..cc01e8f5 100644
--- a/src/Microsoft.Android.Build.BaseTasks/AndroidToolTask.cs
+++ b/src/Microsoft.Android.Build.BaseTasks/AndroidToolTask.cs
@@ -1,6 +1,7 @@
 // https://github.com/xamarin/xamarin-android/blob/9fca138604c53989e1cff7fc0c2e939583b4da28/src/Xamarin.Android.Build.Tasks/Tasks/AndroidTask.cs#L75
 
 using System;
+using System.IO;
 using Microsoft.Build.Utilities;
 
 namespace Microsoft.Android.Build.Tasks
@@ -9,6 +10,13 @@ public abstract class AndroidToolTask : ToolTask
 	{
 		public abstract string TaskPrefix { get; }
 
+		protected string WorkingDirectory { get; private set; }
+
+		public AndroidToolTask ()
+		{
+			WorkingDirectory = Directory.GetCurrentDirectory();
+		}
+
 		public override bool Execute ()
 		{
 			try {
@@ -22,5 +30,7 @@ public override bool Execute ()
 		// Most ToolTask's do not override Execute and
 		// just expect the base to be called
 		public virtual bool RunTask () => base.Execute ();
+
+		protected object ProjectSpecificTaskObjectKey (object key) => (key, WorkingDirectory);
 	}
 }
diff --git a/src/Microsoft.Android.Build.BaseTasks/MSBuildExtensions.cs b/src/Microsoft.Android.Build.BaseTasks/MSBuildExtensions.cs
index d50b3d93..b467e833 100644
--- a/src/Microsoft.Android.Build.BaseTasks/MSBuildExtensions.cs
+++ b/src/Microsoft.Android.Build.BaseTasks/MSBuildExtensions.cs
@@ -251,18 +251,30 @@ public static void SetDestinationSubPath (this ITaskItem assembly)
 
 		/// <summary>
 		/// IBuildEngine4.RegisterTaskObject, but adds the current assembly path into the key
+		/// The `key` should be unique to a project unless it is a global item.
+		/// Ideally the key should be the full path of a file in the project directory structure.
+		/// Or you can use the `ProjectSpecificTaskObjectKey` method of the `AndroidTask` to generate
+		/// a project specific key if needed.
 		/// </summary>
 		public static void RegisterTaskObjectAssemblyLocal (this IBuildEngine4 engine, object key, object value, RegisteredTaskObjectLifetime lifetime, bool allowEarlyCollection = false) =>
 			engine.RegisterTaskObject ((AssemblyLocation, key), value, lifetime, allowEarlyCollection);
 
 		/// <summary>
 		/// IBuildEngine4.GetRegisteredTaskObject, but adds the current assembly path into the key
+		/// The `key` should be unique to a project unless it is a global item.
+		/// Ideally the key should be the full path of a file in the project directory structure.
+		/// Or you can use the `ProjectSpecificTaskObjectKey` method of the `AndroidTask` to generate
+		/// a project specific key if needed.
 		/// </summary>
 		public static object GetRegisteredTaskObjectAssemblyLocal (this IBuildEngine4 engine, object key, RegisteredTaskObjectLifetime lifetime) =>
 			engine.GetRegisteredTaskObject ((AssemblyLocation, key), lifetime);
 
 		/// <summary>
 		/// Generic version of IBuildEngine4.GetRegisteredTaskObject, but adds the current assembly path into the key
+		/// The `key` should be unique to a project unless it is a global item.
+		/// Ideally the key should be the full path of a file in the project directory structure.
+		/// Or you can use the `ProjectSpecificTaskObjectKey` method of the `AndroidTask` to generate
+		/// a project specific key if needed.
 		/// </summary>
 		public static T GetRegisteredTaskObjectAssemblyLocal<T> (this IBuildEngine4 engine, object key, RegisteredTaskObjectLifetime lifetime)
 			where T : class =>
@@ -271,12 +283,20 @@ public static T GetRegisteredTaskObjectAssemblyLocal<T> (this IBuildEngine4 engi
 
 		/// <summary>
 		/// IBuildEngine4.UnregisterTaskObject, but adds the current assembly path into the key
+		/// The `key` should be unique to a project unless it is a global item.
+		/// Ideally the key should be the full path of a file in the project directory structure.
+		/// Or you can use the `ProjectSpecificTaskObjectKey` method of the `AndroidTask` to generate
+		/// a project specific key if needed.
 		/// </summary>
 		public static object UnregisterTaskObjectAssemblyLocal (this IBuildEngine4 engine, object key, RegisteredTaskObjectLifetime lifetime) =>
 			engine.UnregisterTaskObject ((AssemblyLocation, key), lifetime);
 
 		/// <summary>
-		/// Generic version of IBuildEngine4.UnregisterTaskObject, but adds the current assembly path into the key
+		/// Generic version of IBuildEngine4.UnregisterTaskObject, but adds the current assembly path into the key.
+		/// The `key` should be unique to a project unless it is a global item.
+		/// Ideally the key should be the full path of a file in the project directory structure.
+		/// Or you can use the `ProjectSpecificTaskObjectKey` method of the `AndroidTask` to generate
+		/// a project specific key if needed.
 		/// </summary>
 		public static T UnregisterTaskObjectAssemblyLocal<T> (this IBuildEngine4 engine, object key, RegisteredTaskObjectLifetime lifetime)
 			where T : class =>
diff --git a/tests/Microsoft.Android.Build.BaseTasks-Tests/AndroidToolTaskTests.cs b/tests/Microsoft.Android.Build.BaseTasks-Tests/AndroidToolTaskTests.cs
new file mode 100644
index 00000000..8b35ce5b
--- /dev/null
+++ b/tests/Microsoft.Android.Build.BaseTasks-Tests/AndroidToolTaskTests.cs
@@ -0,0 +1,100 @@
+using System.IO;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.Android.Build.BaseTasks.Tests.Utilities;
+using Microsoft.Android.Build.Tasks;
+using NUnit.Framework;
+using Microsoft.Build.Framework;
+using Xamarin.Build;
+
+namespace Microsoft.Android.Build.BaseTasks.Tests
+{
+	[TestFixture]
+	public class AndroidToolTaskTests
+	{
+		public class MyAndroidTask : AndroidTask {
+			public override string TaskPrefix {get;} = "MAT";
+			public string Key { get; set; }
+			public string Value { get; set; }
+			public bool ProjectSpecific { get; set; } = false;
+			public override bool RunTask ()
+			{
+				var key = ProjectSpecific ? ProjectSpecificTaskObjectKey (Key) : (Key, (object)string.Empty);
+				BuildEngine4.RegisterTaskObjectAssemblyLocal (key, Value, RegisteredTaskObjectLifetime.Build);
+				return true;
+			}
+		}
+
+		public class MyOtherAndroidTask : AndroidTask {
+			public override string TaskPrefix {get;} = "MOAT";
+			public string Key { get; set; }
+			public bool ProjectSpecific { get; set; } = false;
+
+			[Output]
+			public string Value { get; set; }
+			public override bool RunTask ()
+			{
+				var key = ProjectSpecific ? ProjectSpecificTaskObjectKey (Key) : (Key, (object)string.Empty);
+				Value = BuildEngine4.GetRegisteredTaskObjectAssemblyLocal<string> (key, RegisteredTaskObjectLifetime.Build);
+				return true;
+			}
+		}
+
+		[Test]
+		[TestCase (true, true, true)]
+		[TestCase (false, false, true)]
+		[TestCase (true, false, false)]
+		[TestCase (false, true, false)]
+		public void TestRegisterTaskObjectCanRetrieveCorrectItem (bool projectSpecificA, bool projectSpecificB, bool expectedResult)
+		{
+			var engine = new MockBuildEngine (TestContext.Out) {
+			};
+			var task = new MyAndroidTask () {
+				BuildEngine = engine,
+				Key = "Foo",
+				Value = "Foo",
+				ProjectSpecific = projectSpecificA,
+			};
+			task.Execute ();
+			var task2 = new MyOtherAndroidTask () {
+				BuildEngine = engine,
+				Key = "Foo",
+				ProjectSpecific = projectSpecificB,
+			};
+			task2.Execute ();
+			Assert.AreEqual (expectedResult, string.Compare (task2.Value, task.Value, ignoreCase: true) == 0);
+		}
+
+		[Test]
+		[TestCase (true, true, false)]
+		[TestCase (false, false, true)]
+		[TestCase (true, false, false)]
+		[TestCase (false, true, false)]
+		public void TestRegisterTaskObjectFailsWhenDirectoryChanges (bool projectSpecificA, bool projectSpecificB, bool expectedResult)
+		{
+			var engine = new MockBuildEngine (TestContext.Out) {
+			};
+			MyAndroidTask task;
+			var currentDir = Directory.GetCurrentDirectory ();
+			Directory.SetCurrentDirectory (Path.Combine (currentDir, ".."));
+			try {
+				task = new MyAndroidTask () {
+					BuildEngine = engine,
+					Key = "Foo",
+					Value = "Foo",
+					ProjectSpecific = projectSpecificA,
+				};
+			} finally {
+				Directory.SetCurrentDirectory (currentDir);
+			}
+			task.Execute ();
+			var task2 = new MyOtherAndroidTask () {
+				BuildEngine = engine,
+				Key = "Foo",
+				ProjectSpecific = projectSpecificB,
+			};
+			task2.Execute ();
+			Assert.AreEqual (expectedResult, string.Compare (task2.Value, task.Value, ignoreCase: true) == 0);
+		}
+	}
+}
diff --git a/tests/Microsoft.Android.Build.BaseTasks-Tests/Utilites/MockBuildEngine.cs b/tests/Microsoft.Android.Build.BaseTasks-Tests/Utilites/MockBuildEngine.cs
new file mode 100644
index 00000000..38155fcf
--- /dev/null
+++ b/tests/Microsoft.Android.Build.BaseTasks-Tests/Utilites/MockBuildEngine.cs
@@ -0,0 +1,102 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using Microsoft.Build.Framework;
+
+namespace Microsoft.Android.Build.BaseTasks.Tests.Utilities {
+	public class MockBuildEngine : IBuildEngine, IBuildEngine2, IBuildEngine3, IBuildEngine4 {
+		public MockBuildEngine (TextWriter output, IList<BuildErrorEventArgs> errors = null, IList<BuildWarningEventArgs> warnings = null, IList<BuildMessageEventArgs> messages = null, IList<CustomBuildEventArgs> customEvents = null)
+		{
+			this.Output = output;
+			this.Errors = errors;
+			this.Warnings = warnings;
+			this.Messages = messages;
+			this.CustomEvents = customEvents;
+		}
+
+		private TextWriter Output { get; }
+
+		private IList<BuildErrorEventArgs> Errors { get; }
+
+		private IList<BuildWarningEventArgs> Warnings { get; }
+
+		private IList<BuildMessageEventArgs> Messages { get; }
+
+		private IList<CustomBuildEventArgs> CustomEvents { get; }
+
+		int IBuildEngine.ColumnNumberOfTaskNode => -1;
+
+		bool IBuildEngine.ContinueOnError => false;
+
+		int IBuildEngine.LineNumberOfTaskNode => -1;
+
+		string IBuildEngine.ProjectFileOfTaskNode => "this.xml";
+
+		bool IBuildEngine2.IsRunningMultipleNodes => false;
+
+		bool IBuildEngine.BuildProjectFile (string projectFileName, string [] targetNames, IDictionary globalProperties, IDictionary targetOutputs) => true;
+
+		void IBuildEngine.LogCustomEvent (CustomBuildEventArgs e)
+		{
+			this.Output.WriteLine ($"Custom: {e.Message}");
+			if (CustomEvents != null)
+				CustomEvents.Add (e);
+		}
+
+		void IBuildEngine.LogErrorEvent (BuildErrorEventArgs e)
+		{
+			this.Output.WriteLine ($"Error: {e.Message}");
+			if (Errors != null)
+				Errors.Add (e);
+		}
+
+		void IBuildEngine.LogMessageEvent (BuildMessageEventArgs e)
+		{
+			this.Output.WriteLine ($"Message: {e.Message}");
+			if (Messages != null)
+				Messages.Add (e);
+		}
+
+		void IBuildEngine.LogWarningEvent (BuildWarningEventArgs e)
+		{
+			this.Output.WriteLine ($"Warning: {e.Message}");
+			if (Warnings != null)
+				Warnings.Add (e);
+		}
+
+		private Dictionary<object, object> Tasks = new Dictionary<object, object> ();
+
+		void IBuildEngine4.RegisterTaskObject (object key, object obj, RegisteredTaskObjectLifetime lifetime, bool allowEarlyCollection)
+		{
+			Tasks.Add (key, obj);
+		}
+
+		object IBuildEngine4.GetRegisteredTaskObject (object key, RegisteredTaskObjectLifetime lifetime)
+		{
+			object obj = null;
+			Tasks.TryGetValue (key, out obj);
+			return obj;
+		}
+
+		object IBuildEngine4.UnregisterTaskObject (object key, RegisteredTaskObjectLifetime lifetime)
+		{
+			var obj = Tasks [key];
+			Tasks.Remove (key);
+			return obj;
+		}
+
+		BuildEngineResult IBuildEngine3.BuildProjectFilesInParallel (string [] projectFileNames, string [] targetNames, IDictionary [] globalProperties, IList<string> [] removeGlobalProperties, string [] toolsVersion, bool returnTargetOutputs)
+		{
+			throw new NotImplementedException ();
+		}
+
+		void IBuildEngine3.Yield () { }
+
+		void IBuildEngine3.Reacquire () { }
+
+		bool IBuildEngine2.BuildProjectFile (string projectFileName, string [] targetNames, IDictionary globalProperties, IDictionary targetOutputs, string toolsVersion) => true;
+
+		bool IBuildEngine2.BuildProjectFilesInParallel (string [] projectFileNames, string [] targetNames, IDictionary [] globalProperties, IDictionary [] targetOutputsPerProject, string [] toolsVersion, bool useResultsCache, bool unloadProjectsOnCompletion) => true;
+	}
+}
