diff --git a/lib/manager/nuget/__snapshots__/artifacts.spec.ts.snap b/lib/manager/nuget/__snapshots__/artifacts.spec.ts.snap
new file mode 100644
index 0000000000000000000000000000000000000000..95c1fb964c960e37ff675f05e660e2cf644110db
--- /dev/null
+++ b/lib/manager/nuget/__snapshots__/artifacts.spec.ts.snap
@@ -0,0 +1,204 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`updateArtifacts aborts if lock file is unchanged 1`] = `
+Array [
+  Object {
+    "cmd": "dotnet restore project.csproj --force-evaluate",
+    "options": Object {
+      "cwd": "/tmp/github/some/repo",
+      "encoding": "utf-8",
+      "env": Object {
+        "HOME": "/home/user",
+        "HTTPS_PROXY": "https://example.com",
+        "HTTP_PROXY": "http://example.com",
+        "LANG": "en_US.UTF-8",
+        "LC_ALL": "en_US",
+        "NO_PROXY": "localhost",
+        "PATH": "/tmp/path",
+      },
+      "maxBuffer": 10485760,
+      "timeout": 900000,
+    },
+  },
+]
+`;
+
+exports[`updateArtifacts aborts if no lock file found 1`] = `Array []`;
+
+exports[`updateArtifacts authenticates at registries 1`] = `
+Array [
+  Object {
+    "cmd": "dotnet nuget update source myRegistry --username some-username --password some-password --store-password-in-clear-text",
+    "options": Object {
+      "cwd": "/tmp/github/some/repo",
+      "encoding": "utf-8",
+      "env": Object {
+        "HOME": "/home/user",
+        "HTTPS_PROXY": "https://example.com",
+        "HTTP_PROXY": "http://example.com",
+        "LANG": "en_US.UTF-8",
+        "LC_ALL": "en_US",
+        "NO_PROXY": "localhost",
+        "PATH": "/tmp/path",
+      },
+      "maxBuffer": 10485760,
+      "timeout": 900000,
+    },
+  },
+  Object {
+    "cmd": "dotnet restore project.csproj --force-evaluate",
+    "options": Object {
+      "cwd": "/tmp/github/some/repo",
+      "encoding": "utf-8",
+      "env": Object {
+        "HOME": "/home/user",
+        "HTTPS_PROXY": "https://example.com",
+        "HTTP_PROXY": "http://example.com",
+        "LANG": "en_US.UTF-8",
+        "LC_ALL": "en_US",
+        "NO_PROXY": "localhost",
+        "PATH": "/tmp/path",
+      },
+      "maxBuffer": 10485760,
+      "timeout": 900000,
+    },
+  },
+  Object {
+    "cmd": "dotnet nuget update source myRegistry --username '' --password '' --store-password-in-clear-text",
+    "options": Object {
+      "cwd": "/tmp/github/some/repo",
+      "encoding": "utf-8",
+      "env": Object {
+        "HOME": "/home/user",
+        "HTTPS_PROXY": "https://example.com",
+        "HTTP_PROXY": "http://example.com",
+        "LANG": "en_US.UTF-8",
+        "LC_ALL": "en_US",
+        "NO_PROXY": "localhost",
+        "PATH": "/tmp/path",
+      },
+      "maxBuffer": 10485760,
+      "timeout": 900000,
+    },
+  },
+]
+`;
+
+exports[`updateArtifacts catches errors 1`] = `
+Array [
+  Object {
+    "artifactError": Object {
+      "lockFile": "packages.lock.json",
+      "stderr": "not found",
+    },
+  },
+]
+`;
+
+exports[`updateArtifacts does not update lock file when no deps changed 1`] = `Array []`;
+
+exports[`updateArtifacts does not update lock file when non-proj file is changed 1`] = `Array []`;
+
+exports[`updateArtifacts performs lock file maintenance 1`] = `
+Array [
+  Object {
+    "cmd": "dotnet restore project.csproj --force-evaluate",
+    "options": Object {
+      "cwd": "/tmp/github/some/repo",
+      "encoding": "utf-8",
+      "env": Object {
+        "HOME": "/home/user",
+        "HTTPS_PROXY": "https://example.com",
+        "HTTP_PROXY": "http://example.com",
+        "LANG": "en_US.UTF-8",
+        "LC_ALL": "en_US",
+        "NO_PROXY": "localhost",
+        "PATH": "/tmp/path",
+      },
+      "maxBuffer": 10485760,
+      "timeout": 900000,
+    },
+  },
+]
+`;
+
+exports[`updateArtifacts supports docker mode 1`] = `
+Array [
+  Object {
+    "cmd": "docker pull renovate/dotnet",
+    "options": Object {
+      "encoding": "utf-8",
+    },
+  },
+  Object {
+    "cmd": "docker ps --filter name=renovate_dotnet -aq",
+    "options": Object {
+      "encoding": "utf-8",
+    },
+  },
+  Object {
+    "cmd": "docker run --rm --name=renovate_dotnet --label=renovate_child --user=foobar -v \\"/tmp/github/some/repo\\":\\"/tmp/github/some/repo\\" -v \\"/tmp/renovate/cache\\":\\"/tmp/renovate/cache\\" -w \\"/tmp/github/some/repo\\" renovate/dotnet bash -l -c \\"dotnet restore project.csproj --force-evaluate\\"",
+    "options": Object {
+      "cwd": "/tmp/github/some/repo",
+      "encoding": "utf-8",
+      "env": Object {
+        "HOME": "/home/user",
+        "HTTPS_PROXY": "https://example.com",
+        "HTTP_PROXY": "http://example.com",
+        "LANG": "en_US.UTF-8",
+        "LC_ALL": "en_US",
+        "NO_PROXY": "localhost",
+        "PATH": "/tmp/path",
+      },
+      "maxBuffer": 10485760,
+      "timeout": 900000,
+    },
+  },
+]
+`;
+
+exports[`updateArtifacts supports global mode 1`] = `
+Array [
+  Object {
+    "cmd": "dotnet restore project.csproj --force-evaluate",
+    "options": Object {
+      "cwd": "/tmp/github/some/repo",
+      "encoding": "utf-8",
+      "env": Object {
+        "HOME": "/home/user",
+        "HTTPS_PROXY": "https://example.com",
+        "HTTP_PROXY": "http://example.com",
+        "LANG": "en_US.UTF-8",
+        "LC_ALL": "en_US",
+        "NO_PROXY": "localhost",
+        "PATH": "/tmp/path",
+      },
+      "maxBuffer": 10485760,
+      "timeout": 900000,
+    },
+  },
+]
+`;
+
+exports[`updateArtifacts updates lock file 1`] = `
+Array [
+  Object {
+    "cmd": "dotnet restore project.csproj --force-evaluate",
+    "options": Object {
+      "cwd": "/tmp/github/some/repo",
+      "encoding": "utf-8",
+      "env": Object {
+        "HOME": "/home/user",
+        "HTTPS_PROXY": "https://example.com",
+        "HTTP_PROXY": "http://example.com",
+        "LANG": "en_US.UTF-8",
+        "LC_ALL": "en_US",
+        "NO_PROXY": "localhost",
+        "PATH": "/tmp/path",
+      },
+      "maxBuffer": 10485760,
+      "timeout": 900000,
+    },
+  },
+]
+`;
diff --git a/lib/manager/nuget/artifacts.spec.ts b/lib/manager/nuget/artifacts.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..20670eda4c3e3c83a27d61f7c9070382620b30cf
--- /dev/null
+++ b/lib/manager/nuget/artifacts.spec.ts
@@ -0,0 +1,214 @@
+import { exec as _exec } from 'child_process';
+import { join } from 'upath';
+import { envMock, mockExecAll } from '../../../test/execUtil';
+import { fs, mocked } from '../../../test/util';
+import { setUtilConfig } from '../../util';
+import { BinarySource } from '../../util/exec/common';
+import * as docker from '../../util/exec/docker';
+import * as _env from '../../util/exec/env';
+import * as _hostRules from '../../util/host-rules';
+import * as nuget from './artifacts';
+import { determineRegistries as _determineRegistries } from './util';
+
+jest.mock('child_process');
+jest.mock('../../util/exec/env');
+jest.mock('../../util/fs');
+jest.mock('../../util/host-rules');
+jest.mock('./util');
+
+const exec: jest.Mock<typeof _exec> = _exec as any;
+const env = mocked(_env);
+const determineRegistries: jest.Mock<typeof _determineRegistries> = _determineRegistries as any;
+const hostRules = mocked(_hostRules);
+
+const config = {
+  // `join` fixes Windows CI
+  localDir: join('/tmp/github/some/repo'),
+  cacheDir: join('/tmp/renovate/cache'),
+  dockerUser: 'foobar',
+};
+
+describe('updateArtifacts', () => {
+  beforeEach(async () => {
+    jest.resetAllMocks();
+    jest.resetModules();
+    env.getChildProcessEnv.mockReturnValue(envMock.basic);
+    await setUtilConfig(config);
+    docker.resetPrefetchedImages();
+  });
+  it('aborts if no lock file found', async () => {
+    const execSnapshots = mockExecAll(exec);
+    fs.getSiblingFileName.mockReturnValueOnce('packages.lock.json');
+    expect(
+      await nuget.updateArtifacts({
+        packageFileName: 'project.csproj',
+        updatedDeps: ['foo', 'bar'],
+        newPackageFileContent: '{}',
+        config,
+      })
+    ).toBeNull();
+    expect(execSnapshots).toMatchSnapshot();
+  });
+  it('aborts if lock file is unchanged', async () => {
+    const execSnapshots = mockExecAll(exec);
+    fs.getSiblingFileName.mockReturnValueOnce('packages.lock.json');
+    fs.readLocalFile.mockResolvedValueOnce('Current packages.lock.json' as any);
+    fs.readLocalFile.mockResolvedValueOnce('Current packages.lock.json' as any);
+    expect(
+      await nuget.updateArtifacts({
+        packageFileName: 'project.csproj',
+        updatedDeps: ['foo', 'bar'],
+        newPackageFileContent: '{}',
+        config,
+      })
+    ).toBeNull();
+    expect(execSnapshots).toMatchSnapshot();
+  });
+  it('updates lock file', async () => {
+    const execSnapshots = mockExecAll(exec);
+    fs.getSiblingFileName.mockReturnValueOnce('packages.lock.json');
+    fs.readLocalFile.mockResolvedValueOnce('Current packages.lock.json' as any);
+    fs.readLocalFile.mockResolvedValueOnce('New packages.lock.json' as any);
+    expect(
+      await nuget.updateArtifacts({
+        packageFileName: 'project.csproj',
+        updatedDeps: ['dep'],
+        newPackageFileContent: '{}',
+        config,
+      })
+    ).not.toBeNull();
+    expect(execSnapshots).toMatchSnapshot();
+  });
+  it('does not update lock file when non-proj file is changed', async () => {
+    const execSnapshots = mockExecAll(exec);
+    fs.getSiblingFileName.mockReturnValueOnce('packages.lock.json');
+    fs.readLocalFile.mockResolvedValueOnce('Current packages.lock.json' as any);
+    fs.readLocalFile.mockResolvedValueOnce('New packages.lock.json' as any);
+    expect(
+      await nuget.updateArtifacts({
+        packageFileName: 'otherfile.props',
+        updatedDeps: ['dep'],
+        newPackageFileContent: '{}',
+        config,
+      })
+    ).toBeNull();
+    expect(execSnapshots).toMatchSnapshot();
+  });
+  it('does not update lock file when no deps changed', async () => {
+    const execSnapshots = mockExecAll(exec);
+    fs.getSiblingFileName.mockReturnValueOnce('packages.lock.json');
+    fs.readLocalFile.mockResolvedValueOnce('Current packages.lock.json' as any);
+    fs.readLocalFile.mockResolvedValueOnce('New packages.lock.json' as any);
+    expect(
+      await nuget.updateArtifacts({
+        packageFileName: 'project.csproj',
+        updatedDeps: [],
+        newPackageFileContent: '{}',
+        config,
+      })
+    ).toBeNull();
+    expect(execSnapshots).toMatchSnapshot();
+  });
+  it('performs lock file maintenance', async () => {
+    const execSnapshots = mockExecAll(exec);
+    fs.getSiblingFileName.mockReturnValueOnce('packages.lock.json');
+    fs.readLocalFile.mockResolvedValueOnce('Current packages.lock.json' as any);
+    fs.readLocalFile.mockResolvedValueOnce('New packages.lock.json' as any);
+    expect(
+      await nuget.updateArtifacts({
+        packageFileName: 'project.csproj',
+        updatedDeps: [],
+        newPackageFileContent: '{}',
+        config: {
+          ...config,
+          isLockFileMaintenance: true,
+        },
+      })
+    ).not.toBeNull();
+    expect(execSnapshots).toMatchSnapshot();
+  });
+
+  it('supports docker mode', async () => {
+    jest.spyOn(docker, 'removeDanglingContainers').mockResolvedValueOnce();
+    await setUtilConfig({ ...config, binarySource: BinarySource.Docker });
+    const execSnapshots = mockExecAll(exec);
+    fs.getSiblingFileName.mockReturnValueOnce('packages.lock.json');
+    fs.readLocalFile.mockResolvedValueOnce('Current packages.lock.json' as any);
+    fs.readLocalFile.mockResolvedValueOnce('New packages.lock.json' as any);
+    expect(
+      await nuget.updateArtifacts({
+        packageFileName: 'project.csproj',
+        updatedDeps: ['dep'],
+        newPackageFileContent: '{}',
+        config,
+      })
+    ).not.toBeNull();
+    expect(execSnapshots).toMatchSnapshot();
+  });
+  it('supports global mode', async () => {
+    const execSnapshots = mockExecAll(exec);
+    fs.getSiblingFileName.mockReturnValueOnce('packages.lock.json');
+    fs.readLocalFile.mockResolvedValueOnce('Current packages.lock.json' as any);
+    fs.readLocalFile.mockResolvedValueOnce('New packages.lock.json' as any);
+    expect(
+      await nuget.updateArtifacts({
+        packageFileName: 'project.csproj',
+        updatedDeps: ['dep'],
+        newPackageFileContent: '{}',
+        config: {
+          ...config,
+          binarySource: BinarySource.Global,
+        },
+      })
+    ).not.toBeNull();
+    expect(execSnapshots).toMatchSnapshot();
+  });
+  it('catches errors', async () => {
+    fs.getSiblingFileName.mockReturnValueOnce('packages.lock.json');
+    fs.readLocalFile.mockResolvedValueOnce('Current packages.lock.json' as any);
+    fs.writeLocalFile.mockImplementationOnce(() => {
+      throw new Error('not found');
+    });
+    expect(
+      await nuget.updateArtifacts({
+        packageFileName: 'project.csproj',
+        updatedDeps: ['dep'],
+        newPackageFileContent: '{}',
+        config,
+      })
+    ).toMatchSnapshot();
+  });
+  it('authenticates at registries', async () => {
+    const execSnapshots = mockExecAll(exec);
+    fs.getSiblingFileName.mockReturnValueOnce('packages.lock.json');
+    fs.readLocalFile.mockResolvedValueOnce('Current packages.lock.json' as any);
+    fs.readLocalFile.mockResolvedValueOnce('New packages.lock.json' as any);
+    determineRegistries.mockResolvedValueOnce([
+      {
+        name: 'myRegistry',
+        url: 'https://my-registry.example.org',
+      },
+    ] as never);
+    hostRules.find.mockImplementationOnce((search) => {
+      if (
+        search.hostType === 'nuget' &&
+        search.url === 'https://my-registry.example.org'
+      ) {
+        return {
+          username: 'some-username',
+          password: 'some-password',
+        };
+      }
+      return undefined;
+    });
+    expect(
+      await nuget.updateArtifacts({
+        packageFileName: 'project.csproj',
+        updatedDeps: ['dep'],
+        newPackageFileContent: '{}',
+        config,
+      })
+    ).not.toBeNull();
+    expect(execSnapshots).toMatchSnapshot();
+  });
+});
diff --git a/lib/manager/nuget/artifacts.ts b/lib/manager/nuget/artifacts.ts
new file mode 100644
index 0000000000000000000000000000000000000000..05be802d067eb71377b919f7676084f7ef86fc2a
--- /dev/null
+++ b/lib/manager/nuget/artifacts.ts
@@ -0,0 +1,128 @@
+import { id } from '../../datasource/nuget';
+import { logger } from '../../logger';
+import { ExecOptions, exec } from '../../util/exec';
+import {
+  getSiblingFileName,
+  readLocalFile,
+  writeLocalFile,
+} from '../../util/fs';
+import * as hostRules from '../../util/host-rules';
+import {
+  UpdateArtifact,
+  UpdateArtifactsConfig,
+  UpdateArtifactsResult,
+} from '../common';
+import { determineRegistries } from './util';
+
+async function authenticate(
+  packageFileName: string,
+  config: UpdateArtifactsConfig,
+  cmds: string[]
+): Promise<void> {
+  const registries = (
+    (await determineRegistries(packageFileName, config.localDir)) || []
+  ).filter((registry) => registry.name != null);
+  for (const registry of registries) {
+    const { username, password } = hostRules.find({
+      hostType: id,
+      url: registry.url,
+    });
+    if (username && password) {
+      // Add registry credentials from host rules.
+      cmds.unshift(
+        `dotnet nuget update source ${registry.name} --username ${username} --password ${password} --store-password-in-clear-text`
+      );
+      // Ensure that credentials are removed as soon as not necessary anymore.
+      cmds.push(
+        `dotnet nuget update source ${registry.name} --username '' --password '' --store-password-in-clear-text`
+      );
+    }
+  }
+}
+
+async function runDotnetRestore(
+  packageFileName: string,
+  config: UpdateArtifactsConfig
+): Promise<void> {
+  const execOptions: ExecOptions = {
+    docker: {
+      image: 'renovate/dotnet',
+    },
+  };
+  const cmds = [`dotnet restore ${packageFileName} --force-evaluate`];
+  await authenticate(packageFileName, config, cmds);
+  logger.debug({ cmd: cmds }, 'dotnet command');
+  await exec(cmds, execOptions);
+}
+
+export async function updateArtifacts({
+  packageFileName,
+  newPackageFileContent,
+  config,
+  updatedDeps,
+}: UpdateArtifact): Promise<UpdateArtifactsResult[] | null> {
+  logger.debug(`nuget.updateArtifacts(${packageFileName})`);
+
+  if (!/(?:cs|vb|fs)proj$/i.test(packageFileName)) {
+    // This could be implemented in the future if necessary.
+    // It's not that easy though because the questions which
+    // project file to restore how to determine which lock files
+    // have been changed in such cases.
+    logger.debug(
+      { packageFileName },
+      'Not updating lock file for non project files'
+    );
+    return null;
+  }
+
+  const lockFileName = getSiblingFileName(
+    packageFileName,
+    'packages.lock.json'
+  );
+  const existingLockFileContent = await readLocalFile(lockFileName, 'utf8');
+  if (!existingLockFileContent) {
+    logger.debug(
+      { packageFileName },
+      'No lock file found beneath package file.'
+    );
+    return null;
+  }
+
+  try {
+    if (updatedDeps.length === 0 && config.isLockFileMaintenance !== true) {
+      logger.debug(
+        `Not updating lock file because no deps changed and no lock file maintenance.`
+      );
+      return null;
+    }
+
+    await writeLocalFile(packageFileName, newPackageFileContent);
+
+    await runDotnetRestore(packageFileName, config);
+
+    const newLockFileContent = await readLocalFile(lockFileName, 'utf8');
+    if (existingLockFileContent === newLockFileContent) {
+      logger.debug(`Lock file is unchanged`);
+      return null;
+    }
+    logger.debug('Returning updated lock file');
+    return [
+      {
+        file: {
+          name: lockFileName,
+          contents: await readLocalFile(lockFileName),
+        },
+      },
+    ];
+  } catch (err) {
+    logger.debug({ err }, 'Failed to generate lock file');
+    return [
+      {
+        artifactError: {
+          lockFile: lockFileName,
+          stderr: err.message,
+        },
+      },
+    ];
+  }
+}
diff --git a/lib/manager/nuget/extract.ts b/lib/manager/nuget/extract.ts
index 3f087d2fe23b1f489907db260b375b0995ab057d..2a54f003f2647498b969432a93a9ac88368cfede 100644
--- a/lib/manager/nuget/extract.ts
+++ b/lib/manager/nuget/extract.ts
@@ -1,78 +1,12 @@
-import * as path from 'path';
-import findUp from 'find-up';
 import { XmlDocument } from 'xmldoc';
 import * as datasourceNuget from '../../datasource/nuget';
 import { logger } from '../../logger';
 import { SkipReason } from '../../types';
-import { clone } from '../../util/clone';
-import { readFile } from '../../util/fs';
 import { get } from '../../versioning';
 import * as semverVersioning from '../../versioning/semver';
 import { ExtractConfig, PackageDependency, PackageFile } from '../common';
 import { DotnetToolsManifest } from './types';
-
-async function readFileAsXmlDocument(file: string): Promise<XmlDocument> {
-  try {
-    return new XmlDocument(await readFile(file, 'utf8'));
-  } catch (err) {
-    logger.debug({ err }, `failed to parse '${file}' as XML document`);
-    return undefined;
-  }
-}
-
-async function determineRegistryUrls(
-  packageFile: string,
-  localDir: string
-): Promise<string[]> {
-  // Valid file names taken from https://github.com/NuGet/NuGet.Client/blob/f64621487c0b454eda4b98af853bf4a528bef72a/src/NuGet.Core/NuGet.Configuration/Settings/Settings.cs#L34
-  const nuGetConfigFileNames = ['nuget.config', 'NuGet.config', 'NuGet.Config'];
-  const nuGetConfigPath = await findUp(nuGetConfigFileNames, {
-    cwd: path.dirname(path.join(localDir, packageFile)),
-    type: 'file',
-  });
-
-  if (nuGetConfigPath?.startsWith(localDir) !== true) {
-    return undefined;
-  }
-
-  logger.debug({ nuGetConfigPath }, 'found NuGet.config');
-  const nuGetConfig = await readFileAsXmlDocument(nuGetConfigPath);
-  if (!nuGetConfig) {
-    return undefined;
-  }
-
-  const packageSources = nuGetConfig.childNamed('packageSources');
-  if (!packageSources) {
-    return undefined;
-  }
-
-  const registryUrls = clone(datasourceNuget.defaultRegistryUrls);
-  for (const child of packageSources.children) {
-    if (child.type === 'element') {
-      if (child.name === 'clear') {
-        logger.debug(`clearing registry URLs`);
-        registryUrls.length = 0;
-      } else if (child.name === 'add') {
-        const isHttpUrl = /^https?:\/\//i.test(child.attr.value);
-        if (isHttpUrl) {
-          let registryUrl = child.attr.value;
-          if (child.attr.protocolVersion) {
-            registryUrl += `#protocolVersion=${child.attr.protocolVersion}`;
-          }
-          logger.debug({ registryUrl }, 'adding registry URL');
-          registryUrls.push(registryUrl);
-        } else {
-          logger.debug(
-            { registryUrl: child.attr.value },
-            'ignoring local registry URL'
-          );
-        }
-      }
-      // child.name === 'remove' not supported
-    }
-  }
-  return registryUrls;
-}
+import { determineRegistries } from './util';
 
 /**
  * https://docs.microsoft.com/en-us/nuget/concepts/package-versioning
@@ -128,10 +62,10 @@ export async function extractPackageFile(
   logger.trace({ packageFile }, 'nuget.extractPackageFile()');
   const versioning = get(config.versioning || semverVersioning.id);
 
-  const registryUrls = await determineRegistryUrls(
-    packageFile,
-    config.localDir
-  );
+  const registries = await determineRegistries(packageFile, config.localDir);
+  const registryUrls = registries
+    ? registries.map((registry) => registry.url)
+    : undefined;
 
   if (packageFile.endsWith('.config/dotnet-tools.json')) {
     const deps: PackageDependency[] = [];
diff --git a/lib/manager/nuget/index.ts b/lib/manager/nuget/index.ts
index 32d3de691697fc32be533071982b21d7447e4837..4311964f62c0d502a92af09fda345b4071b77eda 100644
--- a/lib/manager/nuget/index.ts
+++ b/lib/manager/nuget/index.ts
@@ -1,6 +1,7 @@
 import { LANGUAGE_DOT_NET } from '../../constants/languages';
 
 export { extractPackageFile } from './extract';
+export { updateArtifacts } from './artifacts';
 
 export const language = LANGUAGE_DOT_NET;
 
diff --git a/lib/manager/nuget/util.ts b/lib/manager/nuget/util.ts
new file mode 100644
index 0000000000000000000000000000000000000000..90f1af1c49c165560e20ba26a8ed1e5c5cafd33d
--- /dev/null
+++ b/lib/manager/nuget/util.ts
@@ -0,0 +1,82 @@
+import * as path from 'path';
+import findUp from 'find-up';
+import { XmlDocument } from 'xmldoc';
+import * as datasourceNuget from '../../datasource/nuget';
+import { logger } from '../../logger';
+import { readFile } from '../../util/fs';
+
+async function readFileAsXmlDocument(file: string): Promise<XmlDocument> {
+  try {
+    return new XmlDocument(await readFile(file, 'utf8'));
+  } catch (err) {
+    logger.debug({ err }, `failed to parse '${file}' as XML document`);
+    return undefined;
+  }
+}
+
+export interface Registry {
+  readonly url: string;
+  readonly name?: string;
+}
+
+export async function determineRegistries(
+  packageFile: string,
+  localDir: string
+): Promise<Registry[] | undefined> {
+  // Valid file names taken from https://github.com/NuGet/NuGet.Client/blob/f64621487c0b454eda4b98af853bf4a528bef72a/src/NuGet.Core/NuGet.Configuration/Settings/Settings.cs#L34
+  const nuGetConfigFileNames = ['nuget.config', 'NuGet.config', 'NuGet.Config'];
+  const nuGetConfigPath = await findUp(nuGetConfigFileNames, {
+    cwd: path.dirname(path.join(localDir, packageFile)),
+    type: 'file',
+  });
+
+  if (nuGetConfigPath?.startsWith(localDir) !== true) {
+    return undefined;
+  }
+
+  logger.debug({ nuGetConfigPath }, 'found NuGet.config');
+  const nuGetConfig = await readFileAsXmlDocument(nuGetConfigPath);
+  if (!nuGetConfig) {
+    return undefined;
+  }
+
+  const packageSources = nuGetConfig.childNamed('packageSources');
+  if (!packageSources) {
+    return undefined;
+  }
+
+  const registries = datasourceNuget.defaultRegistryUrls.map(
+    (registryUrl) =>
+      ({
+        url: registryUrl,
+      } as Registry)
+  );
+  for (const child of packageSources.children) {
+    if (child.type === 'element') {
+      if (child.name === 'clear') {
+        logger.debug(`clearing registry URLs`);
+        registries.length = 0;
+      } else if (child.name === 'add') {
+        const isHttpUrl = /^https?:\/\//i.test(child.attr.value);
+        if (isHttpUrl) {
+          let registryUrl = child.attr.value;
+          if (child.attr.protocolVersion) {
+            registryUrl += `#protocolVersion=${child.attr.protocolVersion}`;
+          }
+          logger.debug({ registryUrl }, 'adding registry URL');
+          registries.push({
+            name: child.attr.key,
+            url: registryUrl,
+          });
+        } else {
+          logger.debug(
+            { registryUrl: child.attr.value },
+            'ignoring local registry URL'
+          );
+        }
+      }
+      // child.name === 'remove' not supported
+    }
+  }
+  return registries;
+}