O problema é que você está verificando o Ano, Cidade e QsNo no
OutPut
variável após a junção... mas se OutPut for nulo (o que aconteceria se não houver linhas em AllCosts), essas verificações sempre serão falsas, então o par (código, OutPut) será filtrado pela cláusula where. O EF detecta esse fato e gera uma consulta mais eficiente usando apenas uma junção interna. O que você realmente quer fazer é filtrar as linhas candidatas de Custos, em vez de filtrar por pares (código, custo). Para fazer isso, você pode mover seu filtro para cima, para que ele se aplique diretamente à tabela Custos:
var Result = from code in ent.ProductCodes
join cost
in ent.Costs.Where(c => c.Year == Year && c.City == City && c.QsNo == Qsno)
on new { code.Year, code.Code } equals new { cost.Year, cost.Code }
into AllCosts
from OutPut in AllCosts.DefaultIfEmpty()
where code.PageNo == PageNo
select new
{
ProductCode = code.Code
Col6 = OutPut.Price
};