Best way to allow update exceptions on a cell level I've got 2 tables, a base table - tBase - and a holding table used to update that base table - tUpdate.  (Note: actual tables will have around 25 columns)

Table tBase

ID  |  name  |  type  |  color  |  size
----------------------------------------
 1      A        Z        red       big
 2      B        Y        blue      tiny
 3      C        X        red       tiny
 4      D       NULL      NULL      NULL


Table tUpdate

ID  |  name  |  type  |  color  |  size
----------------------------------------
 1      A        Z        red       big
 2      B        Z        blue      tiny
 3      C        X        NULL      tiny
 4      D        W        gold      big
 5      E        Y        grey      cat


I would like to design a way to say specific cells should not be updated, but the best I can come up with at the moment involves multiple updates.  Basically, my idea involves having a third table - tExclude that would look like this:

ID  |  ColName
---------------
 2      type
 4      size
 4      color


The actual UPDATE statements would then run for each column and exclude any IDs that are in the tExclude table.  I'd rather not have to make 25 different updates that use the same 2 tables, and I feel like there's got to be a better way of doing it that I'm not finding.

I've looked into CROSS APPLY a bit but couldn't figure out how it could help.  I also looked into MERGE but that also didn't seem to have a way to do what I'm looking for.
This sounds like the classic use-case for a SQL Server-based cursor.  Yes, I just said the "c" word.

Take this code, which uses your sample data:

SET NOCOUNT ON;

IF OBJECT_ID(N'dbo.tExclude', N'U') IS NOT NULL
DROP TABLE dbo.tExclude;
CREATE TABLE dbo.tExclude
(
    ID int NOT NULL
    , ColName sysname NOT NULL
);

IF OBJECT_ID(N'dbo.tBase', N'U') IS NOT NULL
DROP TABLE dbo.tBase;
CREATE TABLE dbo.tBase
(
    ID int NOT NULL
    , item_name varchar(30)
    , item_type varchar(30)
    , color varchar(30)
    , size varchar(30)
);

IF OBJECT_ID(N'dbo.tUpdate', N'U') IS NOT NULL
DROP TABLE dbo.tUpdate;
CREATE TABLE dbo.tUpdate
(
    ID int NOT NULL
    , item_name varchar(30)
    , item_type varchar(30)
    , color varchar(30)
    , size varchar(30)
);


Sample data:

INSERT INTO dbo.tExclude (ID, ColName)
VALUES (1, 'item_name')
    , (1, 'size');

INSERT INTO dbo.tBase (ID, item_name, item_type, color, size)
VALUES (1, 'A', 'Z', 'red', 'big')
    , (2, 'B', 'Y', 'blue', 'tiny')
    , (3, 'C', 'X', 'red', 'tiny')
    , (4, 'D', NULL, NULL, NULL);

INSERT INTO dbo.tUpdate(ID, item_name, item_type, color, size)
VALUES (1, 'A', 'Z', 'red', 'big')
    , (2, 'B', 'Y', 'blue', 'tiny')
    , (3, 'C', 'X', 'red', 'tiny')
    , (4, 'D', 'W', 'gold', 'big')
    , (5, 'E', 'Y', 'grey', 'cat');


At this point, the dbo.tBase table looks like:

╔════╦═══════════╦═══════════╦═══════╦══════╗
║ ID ║ item_name ║ item_type ║ color ║ size ║
╠════╬═══════════╬═══════════╬═══════╬══════╣
║  1 ║ A         ║ Z         ║ red   ║ big  ║
║  2 ║ B         ║ Y         ║ blue  ║ tiny ║
║  3 ║ C         ║ X         ║ red   ║ tiny ║
║  4 ║ D         ║ NULL      ║ NULL  ║ NULL ║
╚════╩═══════════╩═══════════╩═══════╩══════╝

Code to do the desired updates and inserts:

IF OBJECT_ID(N'tempdb..#t', N'U') IS NOT NULL
DROP TABLE #t;
CREATE TABLE #t 
(
    cmd nvarchar(max) NOT NULL
);

DECLARE @cmd nvarchar(max);
DECLARE @ID int;
DECLARE @Type varchar(6);
DECLARE cur CURSOR LOCAL FORWARD_ONLY STATIC
FOR
SELECT tu.ID
    , CASE WHEN tb.ID IS NULL THEN 'INSERT' ELSE 'UPDATE' END
FROM dbo.tUpdate tu
    LEFT JOIN dbo.tBase tb ON tu.ID = tb.ID;
OPEN cur;
FETCH NEXT FROM cur INTO @ID, @Type;
WHILE @@FETCH_STATUS = 0
BEGIN
    IF @Type = 'UPDATE'
    BEGIN
        SET @cmd = N'
WITH src AS 
(
    SELECT c = QUOTENAME(c.name) + '' = '' + ''tu.'' + QUOTENAME(c.name)
    FROM sys.columns c
        INNER JOIN sys.tables t ON c.object_id = t.object_id
    WHERE t.name = ''tBase''
        AND c.name &lt;&gt; ''ID''
    EXCEPT 
    SELECT QUOTENAME(e.ColName) + '' = '' + ''tu.'' + QUOTENAME(e.ColName)
    FROM dbo.tExclude e
    WHERE ID = ' + CONVERT(nvarchar(max), @ID) + N'
)
SELECT ''UPDATE dbo.tBase
SET '' + (STUFF((SELECT '', '' + src.c FROM src FOR XML PATH ('''')), 1, 2, '''')) + ''
FROM dbo.tBase tb
    INNER JOIN dbo.tUpdate tu ON tb.ID = tu.ID
WHERE tb.ID = ' + CONVERT(nvarchar(max), @ID) + N';''';
        INSERT INTO #t
        EXEC sys.sp_executesql @cmd;
    END
    ELSE
    BEGIN
        SET @cmd = N'INSERT INTO dbo.tBase (ID, item_name, item_type, color, size)
SELECT ID, item_name, item_type, color, size
FROM dbo.tUpdate tu
WHERE tu.ID = ' + CONVERT(nvarchar(max), @ID) + N';';
        INSERT INTO #t (cmd)
        SELECT @cmd;
    END
    FETCH NEXT FROM cur INTO @ID, @Type;
END
CLOSE cur;
DEALLOCATE cur;

DECLARE cur CURSOR LOCAL FORWARD_ONLY STATIC
FOR
SELECT t.cmd
FROM #t t;
OPEN cur;
FETCH NEXT FROM cur INTO @cmd;
WHILE @@FETCH_STATUS = 0
BEGIN
    PRINT @cmd;
    EXEC sys.sp_executesql @cmd;
    PRINT N'';
    FETCH NEXT FROM cur INTO @cmd;
END
CLOSE cur;
DEALLOCATE cur;


The code above uses a couple of cursors to create and execute INSERT or UPDATE statements as required.  The UPDATE statements never update columns for rows included in the dbo.tExclude table.

The generated INSERT/UPDATE statements look like:

UPDATE dbo.tBase
SET [color] = tu.[color], [item_type] = tu.[item_type]
FROM dbo.tBase tb
    INNER JOIN dbo.tUpdate tu ON tb.ID = tu.ID
WHERE tb.ID = 1;

UPDATE dbo.tBase
SET [color] = tu.[color], [item_name] = tu.[item_name], [item_type] = tu.[item_type], [size] = tu.[size]
FROM dbo.tBase tb
    INNER JOIN dbo.tUpdate tu ON tb.ID = tu.ID
WHERE tb.ID = 2;

UPDATE dbo.tBase
SET [color] = tu.[color], [item_name] = tu.[item_name], [item_type] = tu.[item_type], [size] = tu.[size]
FROM dbo.tBase tb
    INNER JOIN dbo.tUpdate tu ON tb.ID = tu.ID
WHERE tb.ID = 3;

UPDATE dbo.tBase
SET [color] = tu.[color], [item_name] = tu.[item_name], [item_type] = tu.[item_type], [size] = tu.[size]
FROM dbo.tBase tb
    INNER JOIN dbo.tUpdate tu ON tb.ID = tu.ID
WHERE tb.ID = 4;

INSERT INTO dbo.tBase (ID, item_name, item_type, color, size)
SELECT ID, item_name, item_type, color, size
FROM dbo.tUpdate tu
WHERE tu.ID = 5;


And the dbo.tBase table now looks like:

SELECT *
FROM dbo.tBase;


╔════╦═══════════╦═══════════╦═══════╦══════╗
║ ID ║ item_name ║ item_type ║ color ║ size ║
╠════╬═══════════╬═══════════╬═══════╬══════╣
║  1 ║ A         ║ Z         ║ red   ║ big  ║
║  2 ║ B         ║ Y         ║ blue  ║ tiny ║
║  3 ║ C         ║ X         ║ red   ║ tiny ║
║  4 ║ D         ║ W         ║ gold  ║ big  ║
║  5 ║ E         ║ Y         ║ grey  ║ cat  ║
╚════╩═══════════╩═══════════╩═══════╩══════╝