Repository: nunit/nunit-console
Pull request #1653: Fix error in dispatch of events when using Windows Remoting
Author: login: CharliePoole
Created: 2025-04-03T19:16:39Z | Merged: 2025-04-04T04:39:36Z

Fixes #1644

Linked issue #1644: NUnit Console v4: not all events are received in ITestEventListener#OnTestEvent.
- I am making **NUnit.Extension.TeamCityEventListener** compatible with NUnit Console Runner v4.
You can check the changes I made [here](https://github.com/kim-vitaliy/teamcity-event-listener/commit/f0323104dead7d327a3b22df2d5a0252f146c1ab).
- Also, I've attached the listener build to the ticket [NUnit.Extension.TeamCityEventListener.zip](https://github.com/user-attachments/files/19227486/NUnit.Extension.TeamCityEventListener.zip)
- I downloaded [NUnit.ConsoleRunner 4.0.0-alpha.31](https://www.myget.org/feed/nunit/package/nuget/NUnit.ConsoleRunner/4.0.0-alpha.31), unpacked it and put the content of the NUnit.Extension.TeamCityEventListener.zip to the top directory of the unpacked tool.

When I call `mono ./tools/nunit-console.exe --list-extensions --teamcity` I see that the extension is discovered:
```
Runtime Environment
    OS Version: MacOSX 23.2.0.0 
    Runtime: .NET Framework CLR v4.0.30319.42000

Installed Extensions
    Extension Point: /NUnit/Engine/TypeExtensions/IService
    Extension Point: /NUnit/Engine/TypeExtensions/ITestEventListener
      Extension: NUnit.Engine.Listeners.TeamCityEventListener
        Version: 1.1.0.0
        Path: /Users/Vitaliy.Kim/Desktop/NUnitMigration/NUnit.ConsoleRunner.4.0.0-alpha.31/NUnit.Extension.TeamCityEventListener/tools/net462/teamcity-event-listener.dll
    Extension Point: /NUnit/Engine/TypeExtensions/IDriverFactory
    Extension Point: /NUnit/Engine/TypeExtensions/IProjectLoader
    Extension Point: /NUnit/Engine/TypeExtensions/IResultWriter
```

But when I run the command specifying the DLL with tests: `mono ./tools/nunit-console.exe /path-to-tests/Tests/bin/Debug/net7.0/HelloWorld.NUnit.dll --result=/path-to-output/1.xml --noheader --teamcity`, I see that events **start-suite**, **start-test**, **test-case**, and **test-suite** do not reach **ITestEventListener#OnTestEvent**.

I am attaching my test project to the ticket: [Tests.zip](https://github.com/user-attachments/files/19227601/Tests.zip).

Additionally, I am including two files comparing the events received in NUnit 3 and NUnit 4.
If you diff their contents, you can see that out of all the events that were received in version 3, only **start-run** and **test-run** events are now received in version 4:
[v3_test_run.txt](https://github.com/user-attachments/files/19227606/v3_test_run.txt)
[v4_test_run.txt](https://github.com/user-attachments/files/19227607/v4_test_run.txt)

Please note that in this build, I have added `Console.Error.WriteLine` statements to the ITestEventListener#OnTestEvent method so that you can see which events are received during testing.
I didn't change the rest of the method's logic.
```
public void OnTestEvent(string report)
        {
            Console.Error.WriteLine(report);
            Console.Error.WriteLine();
            ...
        }
```
Linked issue comments: user: login: CharliePoole
created_at: 2025-03-17T16:15:40Z
body: TeamCity extension is the only ITestEventListener with which we have tested in the past. As part of this issue, we should add a fake test listener extension to our tests and test that it gets all events. This would allow us to detect problems at the unit testing stage. 
author_association: MEMBER
user: login: CharliePoole
created_at: 2025-04-04T04:40:59Z
body: Fixed with PR #1653. Availble from myget in version 4.0.0-alpha.33 or higher.
author_association: MEMBER
user: login: CharliePoole
created_at: 2025-06-26T05:54:17Z
body: <!-- GitReleaseManager release comment -->
This issue has been resolved in version 4.0.0-beta.1

The release is available on:
[GitHub](https://github.com/NUnit/nunit-console/releases/tag/4.0.0-beta.1).
NuGet packages are also available [NuGet.org](https://www.nuget.org) and
Chocolatey Packages may be found at [Chocolatey.org](https://chocolatey.org)
author_association: MEMBER

Diff:
diff --git a/package-tests.cake b/package-tests.cake
index 4e0e4045b..7254fa91f 100644
--- a/package-tests.cake
+++ b/package-tests.cake
@@ -318,7 +318,7 @@ AddToBothLists(new PackageTest(1, "NoExtensionsInstalled")
 AddToBothLists(new PackageTest(1, "ExtensionsInstalledFromAddedDirectory")
 {
     Description = "List Extensions shows extension from added directory",
-    Arguments = "--extensionDirectory ../../src/TestData/FakeExtensions --list-extensions --trace:Debug",
+    Arguments = "--extensionDirectory ../../src/TestData/FakeExtensions --list-extensions",
     OutputCheck = new OutputContains("Extension:", exactly: 5)
 });
 
@@ -326,6 +326,13 @@ AddToBothLists(new PackageTest(1, "ExtensionsInstalledFromAddedDirectory")
 // RUN TESTS USING EACH OF OUR EXTENSIONS
 //////////////////////////////////////////////////////////////////////
 
+StandardRunnerTests.Add(new PackageTest(1, "FakeEventListenerTest")
+{
+    Description = "Test that event listener gets all reports",
+    Arguments = "testdata/net462/mock-assembly.dll --extensionDirectory ../../src/TestData/FakeExtensions --trace:Debug",
+    ExpectedResult = new MockAssemblyExpectedResult("netcore-4.6.2")
+});
+
 // TODO: Add back extension tests after latest changes to ExtensionManager
 // are ported. Most extensions will require an update to work under V4.
 
diff --git a/src/NUnitCommon/nunit.agent.core/Drivers/NUnitFrameworkApi2018.cs b/src/NUnitCommon/nunit.agent.core/Drivers/NUnitFrameworkApi2018.cs
index 711b6804b..6e75de5dd 100644
--- a/src/NUnitCommon/nunit.agent.core/Drivers/NUnitFrameworkApi2018.cs
+++ b/src/NUnitCommon/nunit.agent.core/Drivers/NUnitFrameworkApi2018.cs
@@ -121,7 +121,7 @@ public string Run(ITestEventListener? listener, string filter)
         {
             CheckLoadWasCalled();
             log.Info("Running {0} - see separate log file", Path.GetFileName(_testAssemblyPath.ShouldNotBeNull()));
-            Action<string>? callback = null;
+            Action<string>? callback = listener != null ? listener.OnTestEvent : null;
             return (string)ExecuteMethod(RUN_METHOD, [typeof(Action<string>), typeof(string)], callback, filter);
         }
 
diff --git a/src/NUnitCommon/nunit.agent.core/Drivers/NUnitFrameworkDriver.cs b/src/NUnitCommon/nunit.agent.core/Drivers/NUnitFrameworkDriver.cs
index ef2f36f0c..fe24855da 100644
--- a/src/NUnitCommon/nunit.agent.core/Drivers/NUnitFrameworkDriver.cs
+++ b/src/NUnitCommon/nunit.agent.core/Drivers/NUnitFrameworkDriver.cs
@@ -131,7 +131,10 @@ public string Load(string testAssemblyPath, IDictionary<string, object> settings
         /// <param name="listener">An ITestEventHandler that receives progress notices</param>
         /// <param name="filter">A filter that controls which tests are executed</param>
         /// <returns>An Xml string representing the result</returns>
-        public string Run(ITestEventListener? listener, string filter) => _api.Run(null, filter);
+        public string Run(ITestEventListener? listener, string filter)
+        {
+            return _api.Run(listener != null ? new EventInterceptor(listener) : null, filter);
+        }
 
         /// <summary>
         /// Executes the tests in an assembly asynchronously.
@@ -152,5 +155,72 @@ public string Load(string testAssemblyPath, IDictionary<string, object> settings
         /// <param name="filter">A filter indicating which tests to include</param>
         /// <returns>An Xml string representing the tests</returns>
         public string Explore(string filter) => _api.Explore(filter);
+
+        /// <summary>
+        /// Nested class used to intercept progress reports received from the test
+        /// framework and resend them to the actual listener, normally located in
+        /// the runner that is using the engine.
+        /// </summary>
+        /// <remarks>
+        /// This class is absolutely needed when the 2018 NUnit API is used under
+        /// the .NET Framework using Windows Remoting as a communication protocol.
+        /// In particular, the MarshalByRef object implementing the listener must 
+        /// be convertible to ITestEventListener via the IConvertible interface.
+        /// 
+        /// In other cases, the interceptor is not essential, but we use it anyway
+        /// for several reasons:
+        /// 
+        /// 1. We have no control over the implementation of runners using the engine.
+        ///    They may not implement IConvertible and may not even derive from 
+        ///    MarshaByRefObject.
+        /// 
+        /// 2. The interceptor provides a point of control for checking what events 
+        ///    are received from the framework and for possible future modifications to
+        ///    the events before they are forwarded.
+        /// </remarks>
+        public class EventInterceptor : MarshalByRefObject, ITestEventListener, IConvertible
+        {
+            private ITestEventListener _listener;
+
+            public EventInterceptor(ITestEventListener listener)
+            {
+                _listener = listener;
+            }
+
+            #region ITestEventListener and IConvertible Implementations
+
+            void ITestEventListener.OnTestEvent(string report)
+            {
+                _listener.OnTestEvent(report);
+            }
+
+            // Conversion to ITestEventListener is the only one that makes sense
+            object IConvertible.ToType(Type conversionType, IFormatProvider? provider) =>
+                conversionType == typeof(ITestEventListener) ? this : InvalidCast(conversionType);
+
+            TypeCode IConvertible.GetTypeCode() => TypeCode.Object;
+            bool IConvertible.ToBoolean(IFormatProvider? provider) => InvalidCast<bool>();
+            char IConvertible.ToChar(IFormatProvider? provider) => InvalidCast<char>();
+            sbyte IConvertible.ToSByte(IFormatProvider? provider) => InvalidCast<sbyte>();
+            byte IConvertible.ToByte(IFormatProvider? provider) => InvalidCast<byte>();
+            short IConvertible.ToInt16(IFormatProvider? provider) => InvalidCast<short>();
+            ushort IConvertible.ToUInt16(IFormatProvider? provider) => InvalidCast<ushort>();
+            int IConvertible.ToInt32(IFormatProvider? provider) => InvalidCast<int>();
+            uint IConvertible.ToUInt32(IFormatProvider? provider) => InvalidCast<uint>();
+            long IConvertible.ToInt64(IFormatProvider? provider) => InvalidCast<long>();
+            ulong IConvertible.ToUInt64(IFormatProvider? provider) => InvalidCast<ulong>();
+            float IConvertible.ToSingle(IFormatProvider? provider) => InvalidCast<float>();
+            double IConvertible.ToDouble(IFormatProvider? provider) => InvalidCast<double>();
+            decimal IConvertible.ToDecimal(IFormatProvider? provider) => InvalidCast<decimal>();
+            DateTime IConvertible.ToDateTime(IFormatProvider? provider) => InvalidCast<DateTime>();
+            string IConvertible.ToString(IFormatProvider? provider) => InvalidCast<string>();
+
+            static T InvalidCast<T>() => (T)InvalidCast(typeof(T));
+
+            static object InvalidCast(Type type) =>
+                throw new InvalidCastException($"{nameof(EventInterceptor)} is not convertible to {nameof(type)}");
+
+            #endregion
+        }
     }
 }
diff --git a/src/NUnitCommon/nunit.agent.core/Runners/TestAgentRunner.cs b/src/NUnitCommon/nunit.agent.core/Runners/TestAgentRunner.cs
index 846bdd2aa..ccac1dd0f 100644
--- a/src/NUnitCommon/nunit.agent.core/Runners/TestAgentRunner.cs
+++ b/src/NUnitCommon/nunit.agent.core/Runners/TestAgentRunner.cs
@@ -16,6 +16,8 @@ namespace NUnit.Engine.Runners
     /// </summary>
     public abstract class TestAgentRunner : ITestEngineRunner
     {
+        private readonly Logger log = InternalTrace.GetLogger(typeof(TestAgentRunner));
+
         private readonly ProvidedPathsAssemblyResolver? _assemblyResolver;
 
         private IFrameworkDriver? _driver;
diff --git a/src/NUnitConsole/nunit4-console/TestEventHandler.cs b/src/NUnitConsole/nunit4-console/TestEventHandler.cs
index 95a884ad0..c9ecd31ad 100644
--- a/src/NUnitConsole/nunit4-console/TestEventHandler.cs
+++ b/src/NUnitConsole/nunit4-console/TestEventHandler.cs
@@ -11,11 +11,7 @@ namespace NUnit.ConsoleRunner
     /// TestEventHandler processes events from the running
     /// test for the console runner.
     /// </summary>
-#if NETFRAMEWORK
-    public class TestEventHandler : MarshalByRefObject, ITestEventListener
-#else
     public class TestEventHandler : ITestEventListener
-#endif
     {
         private readonly ExtendedTextWriter _outWriter;
 
@@ -205,12 +201,5 @@ private static ColorStyle GetColorForResultStatus(string status)
                     return ColorStyle.Output;
             }
         }
-
-#if NETFRAMEWORK
-        public override object InitializeLifetimeService()
-        {
-            return null!;
-        }
-#endif
     }
 }
diff --git a/src/TestData/FakeExtensions/FakeExtensions.cs b/src/TestData/FakeExtensions/FakeExtensions.cs
index 0d78168e8..ef31917ac 100644
--- a/src/TestData/FakeExtensions/FakeExtensions.cs
+++ b/src/TestData/FakeExtensions/FakeExtensions.cs
@@ -66,7 +66,9 @@ public class FakeEventListenerExtension : ITestEventListener
     {
         public void OnTestEvent(string report)
         {
-            throw new NotImplementedException();
+            if (report.Length > 63)
+                report = report.Substring(0, 60) + "...";
+            Console.WriteLine($"EventListener: {report}");
         }
     }
 


Reviews:
user: login: CharliePoole
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-03T19:44:45Z
user: login: manfred-brands
body: A few suggested changes mainly because there is in those locations no longer a difference between NETFRAMEWORK and later .NET versions.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-04T01:42:47Z
user: login: CharliePoole
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T02:39:51Z
user: login: CharliePoole
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T02:40:08Z
user: login: CharliePoole
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T02:56:00Z

Review comments:
diff_hunk: @@ -152,5 +155,73 @@ public string Load(string testAssemblyPath, IDictionary<string, object> settings
         /// <param name="filter">A filter indicating which tests to include</param>
         /// <returns>An Xml string representing the tests</returns>
         public string Explore(string filter) => _api.Explore(filter);
+
+        /// <summary>
+        /// Nested class used to intercept progress reports received from the test
+        /// framework and resend them to the actual listener, normally located in
+        /// the runner that is using the engine.
+        /// </summary>
+        /// <remarks>
+        /// This class is absolutely needed when the 2018 NUnit API is used under
+        /// the .NET Framework using Windows Remoting as a communicatioion protocol.
+        /// In particular, the MarshalByRef object implementing the listener must 
+        /// be convertible to ITestEventListener via the IConvertible interface.
+        /// 
+        /// In other cases, the interceptor is not essential, but we use it anyway
+        /// for several reasons:
+        /// 
+        /// 1. We have no control over the implementation of runners using the engine.
+        ///    They may not implement IConvertible and may not even derive from 
+        ///    MarshaByRefObject.
+        /// 
+        /// 2. The interceptor provides a point of control for checking what events 
+        ///    are received from the framework and for possible future modifications to
+        ///    the events before they are forwarded.
+        /// </remarks>
+        public class EventInterceptor : MarshalByRefObject, ITestEventListener, IConvertible
+        {
+            private ITestEventListener _listener;
+
+            public EventInterceptor(ITestEventListener listener)
+            {
+                _listener = listener;
+            }
+
+            #region ITestEventListener and IConvertible Implementations
+
+            void ITestEventListener.OnTestEvent(string report)
+            {
+                _listener.OnTestEvent(report);
+            }
+
+
+            // Conversion to ITestEventListener is the only one that makes sense
+            object IConvertible.ToType(Type conversionType, IFormatProvider? provider) =>
+                conversionType == typeof(ITestEventListener) ? this : InvalidCast(conversionType);
path: src/NUnitCommon/nunit.agent.core/Drivers/NUnitFrameworkDriver.cs
user: login: CharliePoole
body: This code looks odd, since it "converts" the object by returning it as an object, but it turns out that this is what you have to do in this situation. And, of course, you have to implement all the IConvertible methods even though none of them do anything but throw. Hopefully, we'll stop using Windows Remoting at some point and be able to get rid of all this.
created_at: 2025-04-03T19:44:45Z
author_association: MEMBER
diff_hunk: @@ -121,7 +121,11 @@ public string Run(ITestEventListener? listener, string filter)
         {
             CheckLoadWasCalled();
             log.Info("Running {0} - see separate log file", Path.GetFileName(_testAssemblyPath.ShouldNotBeNull()));
-            Action<string>? callback = null;
+#if NETFRAMEWORK
+            Action<string>? callback = listener != null ? listener.OnTestEvent : null;
+#else
+            Action<string>? callback = listener != null ? listener.OnTestEvent : null;
+#endif
path: src/NUnitCommon/nunit.agent.core/Drivers/NUnitFrameworkApi2018.cs
user: login: manfred-brands
body: I don't see any difference between the two?
Note that it can be simplified into:
```suggestion
            Action<string>? callback = listener?.OnTestEvent;
```
created_at: 2025-04-04T01:31:07Z
author_association: MEMBER
diff_hunk: @@ -152,5 +155,73 @@ public string Load(string testAssemblyPath, IDictionary<string, object> settings
         /// <param name="filter">A filter indicating which tests to include</param>
         /// <returns>An Xml string representing the tests</returns>
         public string Explore(string filter) => _api.Explore(filter);
+
+        /// <summary>
+        /// Nested class used to intercept progress reports received from the test
+        /// framework and resend them to the actual listener, normally located in
+        /// the runner that is using the engine.
+        /// </summary>
+        /// <remarks>
+        /// This class is absolutely needed when the 2018 NUnit API is used under
+        /// the .NET Framework using Windows Remoting as a communicatioion protocol.
path: src/NUnitCommon/nunit.agent.core/Drivers/NUnitFrameworkDriver.cs
user: login: manfred-brands
body: ```suggestion
        /// the .NET Framework using Windows Remoting as a communication protocol.
```
created_at: 2025-04-04T01:32:48Z
author_association: MEMBER
diff_hunk: @@ -152,5 +155,73 @@ public string Load(string testAssemblyPath, IDictionary<string, object> settings
         /// <param name="filter">A filter indicating which tests to include</param>
         /// <returns>An Xml string representing the tests</returns>
         public string Explore(string filter) => _api.Explore(filter);
+
+        /// <summary>
+        /// Nested class used to intercept progress reports received from the test
+        /// framework and resend them to the actual listener, normally located in
+        /// the runner that is using the engine.
+        /// </summary>
+        /// <remarks>
+        /// This class is absolutely needed when the 2018 NUnit API is used under
+        /// the .NET Framework using Windows Remoting as a communicatioion protocol.
+        /// In particular, the MarshalByRef object implementing the listener must 
+        /// be convertible to ITestEventListener via the IConvertible interface.
+        /// 
+        /// In other cases, the interceptor is not essential, but we use it anyway
+        /// for several reasons:
+        /// 
+        /// 1. We have no control over the implementation of runners using the engine.
+        ///    They may not implement IConvertible and may not even derive from 
+        ///    MarshaByRefObject.
+        /// 
+        /// 2. The interceptor provides a point of control for checking what events 
+        ///    are received from the framework and for possible future modifications to
+        ///    the events before they are forwarded.
+        /// </remarks>
+        public class EventInterceptor : MarshalByRefObject, ITestEventListener, IConvertible
+        {
+            private ITestEventListener _listener;
+
+            public EventInterceptor(ITestEventListener listener)
+            {
+                _listener = listener;
+            }
+
+            #region ITestEventListener and IConvertible Implementations
+
+            void ITestEventListener.OnTestEvent(string report)
+            {
+                _listener.OnTestEvent(report);
+            }
+
+
path: src/NUnitCommon/nunit.agent.core/Drivers/NUnitFrameworkDriver.cs
user: login: manfred-brands
body: ```suggestion

```
created_at: 2025-04-04T01:34:13Z
author_association: MEMBER
diff_hunk: @@ -12,7 +12,7 @@ namespace NUnit.ConsoleRunner
     /// test for the console runner.
     /// </summary>
 #if NETFRAMEWORK
-    public class TestEventHandler : MarshalByRefObject, ITestEventListener
+    public class TestEventHandler : ITestEventListener
 #else
     public class TestEventHandler : ITestEventListener
 #endif
path: src/NUnitConsole/nunit4-console/TestEventHandler.cs
user: login: manfred-brands
body: #if no longer needed as both cases are now the same.

created_at: 2025-04-04T01:39:00Z
author_association: MEMBER
diff_hunk: @@ -16,6 +16,8 @@ namespace NUnit.Engine.Runners
     /// </summary>
     public abstract class TestAgentRunner : ITestEngineRunner
     {
+        Logger log = InternalTrace.GetLogger(typeof(TestAgentRunner));
+
path: src/NUnitCommon/nunit.agent.core/Runners/TestAgentRunner.cs
user: login: manfred-brands
body: Either remove or if left in for a reason, add "private readonly" keywords and prefix with `_` to match other fields in this class.
created_at: 2025-04-04T01:41:04Z
author_association: MEMBER
diff_hunk: @@ -16,6 +16,8 @@ namespace NUnit.Engine.Runners
     /// </summary>
     public abstract class TestAgentRunner : ITestEngineRunner
     {
+        Logger log = InternalTrace.GetLogger(typeof(TestAgentRunner));
+
path: src/NUnitCommon/nunit.agent.core/Runners/TestAgentRunner.cs
user: login: CharliePoole
body: Agreed on "private readonly" - but it's "log" everywhere else in the app Exception that proves the rule? (What does that mean anyway?) :-)
created_at: 2025-04-04T02:39:51Z
author_association: MEMBER
diff_hunk: @@ -12,7 +12,7 @@ namespace NUnit.ConsoleRunner
     /// test for the console runner.
     /// </summary>
 #if NETFRAMEWORK
-    public class TestEventHandler : MarshalByRefObject, ITestEventListener
+    public class TestEventHandler : ITestEventListener
 #else
     public class TestEventHandler : ITestEventListener
 #endif
path: src/NUnitConsole/nunit4-console/TestEventHandler.cs
user: login: CharliePoole
body: Right
created_at: 2025-04-04T02:40:08Z
author_association: MEMBER
diff_hunk: @@ -121,7 +121,11 @@ public string Run(ITestEventListener? listener, string filter)
         {
             CheckLoadWasCalled();
             log.Info("Running {0} - see separate log file", Path.GetFileName(_testAssemblyPath.ShouldNotBeNull()));
-            Action<string>? callback = null;
+#if NETFRAMEWORK
+            Action<string>? callback = listener != null ? listener.OnTestEvent : null;
+#else
+            Action<string>? callback = listener != null ? listener.OnTestEvent : null;
+#endif
path: src/NUnitCommon/nunit.agent.core/Drivers/NUnitFrameworkApi2018.cs
user: login: CharliePoole
body: Using `?.` gives "method group cannot be made nullable" error.
created_at: 2025-04-04T02:56:00Z
author_association: MEMBER

PR comments:
user: login: CharliePoole
created_at: 2025-04-04T02:48:13Z
body: I'll fix the duplicates and other stuff. I was going to change the code for framework but then made the last-minute decision to leave the interceptor in place for all builds and forgot to take out the #ifs.

Decided to see how GitHub handles suggested changes if I accept them. Won't do that again. Each one is a separate commit and separate build.

Thanks for the review. The JetBrains guys discovered this problem and are anxious for a fix.
author_association: MEMBER
user: login: manfred-brands
created_at: 2025-04-04T05:07:50Z
body: > Decided to see how GitHub handles suggested changes if I accept them. Won't do that again. Each one is a separate commit and separate build.

If you are in the Files tab, you can add each suggestion to a batch and commit them all as one commit/build.
author_association: MEMBER
user: login: CharliePoole
created_at: 2025-04-04T10:01:40Z
body: Hey, good to know. I'll try that. 
author_association: MEMBER