Index: src/wp-includes/class-wp-network.php
===================================================================
--- src/wp-includes/class-wp-network.php	(revision 41324)
+++ src/wp-includes/class-wp-network.php	(working copy)
@@ -148,9 +148,9 @@
 			case 'id':
 				return (int) $this->id;
 			case 'blog_id':
-				return $this->blog_id;
+				return $this->get_main_site_id();
 			case 'site_id':
-				return (int) $this->blog_id;
+				return (int) $this->get_main_site_id();
 		}
 
 		return null;
@@ -202,6 +202,24 @@
 	}
 
 	/**
+	 * Returns the main site ID for the network.
+	 *
+	 * Internal method used by the magic getter for the 'blog_id' and
+	 * 'site_id' properties.
+	 *
+	 * @since 4.9.0
+	 *
+	 * @return string|int Main site ID as numeric string or integer.
+	 */
+	private function get_main_site_id() {
+		if ( empty( $this->blog_id ) ) {
+			$this->blog_id = get_main_site_id( $this->id );
+		}
+
+		return $this->blog_id;
+	}
+
+	/**
 	 * Set the site name assigned to the network if one has not been populated.
 	 *
 	 * @since 4.4.0
Index: src/wp-includes/functions.php
===================================================================
--- src/wp-includes/functions.php	(revision 41324)
+++ src/wp-includes/functions.php	(working copy)
@@ -4392,19 +4392,92 @@
  * Determine whether a site is the main site of the current network.
  *
  * @since 3.0.0
+ * @since 4.9.0 The $network_id parameter has been added.
  *
- * @param int $site_id Optional. Site ID to test. Defaults to current site.
+ * @param int $site_id    Optional. Site ID to test. Defaults to current site.
+ * @param int $network_id Optional. Network ID of the network to check for.
+ *                        Defaults to current network.
  * @return bool True if $site_id is the main site of the network, or if not
  *              running Multisite.
  */
-function is_main_site( $site_id = null ) {
-	if ( ! is_multisite() )
+function is_main_site( $site_id = null, $network_id = null ) {
+	if ( ! is_multisite() ) {
 		return true;
+	}
 
-	if ( ! $site_id )
+	if ( ! $site_id ) {
 		$site_id = get_current_blog_id();
+	}
 
-	return (int) $site_id === (int) get_network()->site_id;
+	return (int) $site_id === (int) get_network( $network_id )->site_id;
+}
+
+/**
+ * Gets the main site ID.
+ *
+ * @since 4.9.0
+ *
+ * @param int $network_id Optional. The ID of the network for which to get the main site.
+ *                        Defaults to the current network.
+ * @return int The ID of the main site.
+ */
+function get_main_site_id( $network_id = null ) {
+	if ( ! is_multisite() ) {
+		return 1;
+	}
+
+	/**
+	 * Filters the main site ID.
+	 *
+	 * Returning anything other than null will effectively short-circuit the function, returning
+	 * the result parsed as an integer immediately.
+	 *
+	 * @since 4.9.0
+	 *
+	 * @param int|null $main_site_id If anything other than null is returned, it is interpreted as the main site ID.
+	 * @param int $network_id The ID of the network for which the main site was detected.
+	 */
+	$main_site_id = apply_filters( 'pre_get_main_site_id', null, $network_id );
+	if ( null !== $main_site_id ) {
+		return (int) $main_site_id;
+	}
+
+	$network = get_network( $network_id );
+	if ( ! $network ) {
+		return 0;
+	}
+
+	if ( ( defined( 'DOMAIN_CURRENT_SITE' ) && defined( 'PATH_CURRENT_SITE' ) && $network->domain === DOMAIN_CURRENT_SITE && $network->path === PATH_CURRENT_SITE )
+	     || ( defined( 'SITE_ID_CURRENT_SITE' ) && $network->id == SITE_ID_CURRENT_SITE ) ) {
+		if ( defined( 'BLOG_ID_CURRENT_SITE' ) ) {
+			return BLOG_ID_CURRENT_SITE;
+		} elseif ( defined( 'BLOGID_CURRENT_SITE' ) ) { // deprecated.
+			return BLOGID_CURRENT_SITE;
+		}
+	}
+
+	$site = get_site();
+	if ( $site->domain === $network->domain && $site->path === $network->path ) {
+		$main_site_id = (int) $site->id;
+	} else {
+		$main_site_id = wp_cache_get( 'network:' . $network->id . ':main_site', 'site-options' );
+		if ( false === $main_site_id ) {
+			$_sites       = get_sites( array(
+				'fields'     => 'ids',
+				'number'     => 1,
+				'domain'     => $network->domain,
+				'path'       => $network->path,
+				'network_id' => $network->id,
+			) );
+			$main_site_id = ! empty( $_sites ) ? array_shift( $_sites ) : 0;
+
+			wp_cache_add( 'network:' . $network->id . ':main_site', $main_site_id, 'site-options' );
+		}
+	}
+
+	$main_site_id = (int) $main_site_id;
+
+	return $main_site_id;
 }
 
 /**
Index: src/wp-includes/ms-load.php
===================================================================
--- src/wp-includes/ms-load.php	(revision 41324)
+++ src/wp-includes/ms-load.php	(working copy)
@@ -135,7 +135,7 @@
 
 /**
  * Retrieves the closest matching site object by its domain and path.
- * 
+ *
  * This will not necessarily return an exact match for a domain and path. Instead, it
  * breaks the domain and path into pieces that are then used to match the closest
  * possibility from a query.
@@ -424,13 +424,7 @@
 
 	// Figure out the current network's main site.
 	if ( empty( $current_site->blog_id ) ) {
-		if ( $current_blog->domain === $current_site->domain && $current_blog->path === $current_site->path ) {
-			$current_site->blog_id = $current_blog->blog_id;
-		} elseif ( ! $current_site->blog_id = wp_cache_get( 'network:' . $current_site->id . ':main_site', 'site-options' ) ) {
-			$current_site->blog_id = $wpdb->get_var( $wpdb->prepare( "SELECT blog_id FROM $wpdb->blogs WHERE domain = %s AND path = %s",
-				$current_site->domain, $current_site->path ) );
-			wp_cache_add( 'network:' . $current_site->id . ':main_site', $current_site->blog_id, 'site-options' );
-		}
+		$current_site->blog_id = get_main_site_id( $current_site->id );
 	}
 
 	return true;
Index: tests/phpunit/tests/multisite/getMainSiteId.php
===================================================================
--- tests/phpunit/tests/multisite/getMainSiteId.php	(revision 0)
+++ tests/phpunit/tests/multisite/getMainSiteId.php	(working copy)
@@ -0,0 +1,100 @@
+<?php
+
+class Tests_Multisite_Get_Main_Site_ID extends WP_UnitTestCase {
+	protected static $network_ids;
+	protected static $site_ids;
+
+	public static function wpSetUpBeforeClass( $factory ) {
+		self::$network_ids = array(
+			'wordpress.org/' => array( 'domain' => 'wordpress.org', 'path' => '/' ),
+			'wp.org/'        => array( 'domain' => 'wp.org',        'path' => '/' ), // A network with no sites.
+		);
+
+		foreach ( self::$network_ids as &$id ) {
+			$id = $factory->network->create( $id );
+		}
+		unset( $id );
+
+		self::$site_ids = array(
+			'www.w.org/'                   => array( 'domain' => 'www.w.org',     'path' => '/' ),
+			'wordpress.org/'               => array( 'domain' => 'wordpress.org', 'path' => '/',     'site_id' => self::$network_ids['wordpress.org/'] ),
+			'wordpress.org/foo/'           => array( 'domain' => 'wordpress.org', 'path' => '/foo/', 'site_id' => self::$network_ids['wordpress.org/'] ),
+		);
+
+		foreach ( self::$site_ids as &$id ) {
+			$id = $factory->blog->create( $id );
+		}
+		unset( $id );
+	}
+
+	public static function wpTearDownAfterClass() {
+		foreach( self::$site_ids as $id ) {
+			wpmu_delete_blog( $id, true );
+		}
+
+		global $wpdb;
+
+		foreach( self::$network_ids as $id ) {
+			$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->sitemeta} WHERE site_id = %d", $id ) );
+			$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->site} WHERE id= %d", $id ) );
+		}
+
+		wp_update_network_site_counts();
+	}
+
+	/**
+	 * @ticket 29684
+	 */
+	public function test_get_main_site_id_on_main_site_returns_self() {
+		$this->assertSame( get_current_blog_id(), get_main_site_id() );
+	}
+
+	/**
+	 * @ticket 29684
+	 */
+	public function test_get_main_site_id_returns_main_site_in_switched_context() {
+		$main_site_id = get_current_blog_id();
+		$other_site_id = self::$site_ids['www.w.org/'];
+
+		switch_to_blog( $other_site_id );
+		$result = get_main_site_id();
+		restore_current_blog();
+
+		$this->assertSame( $main_site_id, $result );
+	}
+
+	/**
+	 * @ticket 29684
+	 */
+	public function test_get_main_site_id_with_different_network_returns_correct_id() {
+		$this->assertSame( self::$site_ids['wordpress.org/'], get_main_site_id( self::$network_ids['wordpress.org/'] ) );
+	}
+
+	/**
+	 * @ticket 29684
+	 */
+	public function test_get_main_site_id_on_network_without_site_returns_0() {
+		$this->assertSame( 0, get_main_site_id( self::$network_ids['wp.org/'] ) );
+	}
+
+	/**
+	 * @ticket 29684
+	 */
+	public function test_get_main_site_id_on_invalid_network_returns_0() {
+		$this->assertSame( 0, get_main_site_id( 333 ) );
+	}
+
+	/**
+	 * @ticket 29684
+	 */
+	public function test_get_main_site_id_filtered() {
+		add_filter( 'pre_get_main_site_id', array( $this, 'filter_get_main_site_id' ) );
+		$result = get_main_site_id();
+
+		$this->assertSame( 333, $result );
+	}
+
+	public function filter_get_main_site_id() {
+		return 333;
+	}
+}
Index: tests/phpunit/tests/multisite/site.php
===================================================================
--- tests/phpunit/tests/multisite/site.php	(revision 41324)
+++ tests/phpunit/tests/multisite/site.php	(working copy)
@@ -741,6 +741,27 @@
 		restore_current_blog();
 	}
 
+	/**
+	 * @ticket 29684
+	 */
+	function test_is_main_site_different_network() {
+		$args = array(
+			'domain' => 'wordpress.org',
+			'path'   => '/',
+		);
+
+		$network_id = self::factory()->network->create( $args );
+
+		$args['site_id'] = $network_id;
+
+		$site_id = self::factory()->blog->create( $args );
+		$other_site_id = self::factory()->blog->create( array_merge( $args, array(
+			'path' => '/foo/',
+		) ) );
+
+		$this->assertTrue( is_main_site( $site_id, $network_id ) );
+	}
+
 	function test_switch_upload_dir() {
 		$this->assertTrue( is_main_site() );
 
