O C# 4.0 introduz um novo tipo: dynamic. dynamic é um tipo estático que contorna os mecanismos de verificação estática de tipos.
Este novo tipo é muito útil para interoperar com:
Porque a verificação estática de tipos é contornada, isto:
dynamic dynamicValue = GetValue();
dynamicValue.Method();
é equivalente a isto:
object objectValue = GetValue();
objectValue
.GetType()
.InvokeMember(
"Method",
BindingFlags.InvokeMethod,
null,
objectValue,
null);
Aparte a memorização “escondida” do call site e alguma resolução dinâmica, dynamic apenas tem melhor aspecto. Qualquer erro de digitação só será descoberto em tempo de execução.
De facto, se estou a escrever o código, eu sei o contrato do que estou a chamar. Não seria bom se o compilador pudesse fazer alguma verificação estática de tipos na interacção com estes objectos dinâmicos?
Imagine-se que o objecto dinâmico que estou a obter na chamada ao método GetValue, alé do método sem parâmetros Method também tem uma propriedade Property do tipo string. Ist quer dizer que, do ponto de vista do código que estou a escrever, o contrato que o objecto dinâmico devolvido por GetValue implementa é:
string Property { get; }
void Method();
Com se trata de um contrato bem definido, eu poderia escrever uma interface para o representar:
interface IValue
{
string Property { get; }
void Method();
}
Se dynamic permitisse a especificação do contrato no formato dynamic(contrato), eu poderia escrever isto:
dynamic(IValue) dynamicValue = GetValue();
dynamicValue.Method();
Ist não quer dizer que o valor por GetValue tem de implementar a interface IValue. Apenas permite ao compilador verificar que dynamicValue.Method() é uma utilização válida de dynamicValue e dynamicValue.OtherMethod() não.
Se a interface IValue já existir, por alguma razão, não há qualquer problema. Mas ter de adicionar um novo tipo à assembly apenas para ser usado em tempo de compilação não me parece correcto. Por isso, dynamic podeia ser um novo tipo de especificação de tipos. Algo como:
dynamic DValue
{
string Property { get; }
void Method();
}
O código passaria a ser escrito assim;
DValue dynamicValue = GetValue();
dynamicValue.Method();
O compilador nunca geraia qualquer IL ou metadados para esta especificação de tipo. Esta especificação de tipo seria usada apenas para o compilador efectuar uma verificação estática dos objectos diâmicos. Como consequência, não faz qualquer sentido ser acessível publicamente, pelo que tal não seria permitido.
Mais uma vez, se a interface IValue (ou qualquer outra definição de tipo) já existir, pode ser usada na definição de tipo dinâmico:
dynamic DValue : IValue, IEnumerable, SomeClass
{
string Property { get; }
void Method();
}
Um benfício acrescido seria IntelliSense.
Tenho recebido reacções mistas em relação a esta proposta. O que acham? Seria algo útil?
Para quem não sabe, eu tenho um sítio web (http://PauloMorgado.NET/) que uso como presença (além dos meus blogues) na web e para testes.
Porque costumo escrever tanto em Português como em Inglês, queria que o sítio tivesse uma versão Portuguesa e uma versão Inglesa. Isto é simples de fazer usando a infra-estrutura de Globalização e Localização ASP.NET.
Mas eu queria mais do que apenas adivinhar a língua do utilizador através da linguagem do browser. Queria algo como os sítios da MSDN e TechNet onde cultura está embebida no URL o que possibilita ao utilizador escolher em que linguagem deseja ver o sítio.
Com o lançamento do ASP.NET Routing, isto é tão simples como escrever um route handler que define a cultura do pedido HTTP e devolve o page handler da página pretendida.
Algo como isto:
public class GlobalizationRouteHandler : global::System.Web.Routing.IRouteHandler
{
System.Globalization.CultureInfo culture;
System.Globalization.CultureInfo uiCulture;
public GlobalizationRouteHandler(System.Globalization.CultureInfo culture)
: this(culture, culture)
{
}
public GlobalizationRouteHandler(CultureInfo culture, CultureInfo uiCulture)
{
if (culture == null)
{
throw new ArgumentNullException("cultureInfo", "cultureInfo is null.");
}
if (uiCulture == null)
{
throw new ArgumentNullException("uiCulture", "uiCulture is null.");
}
this.culture = culture;
this.uiCulture = uiCulture;
}
private GlobalizationRouteHandler()
{
}
#region IRouteHandler Members
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
Thread.CurrentThread.CurrentCulture = this.culture;
Thread.CurrentThread.CurrentUICulture = this.uiCulture;
string path = "~/" + (requestContext.RouteData.Values["path"] as string);
var physicalPath = requestContext.HttpContext.Server.MapPath(path);
if (System.IO.Directory.Exists(physicalPath))
{
path = VirtualPathUtility.Combine(path, "Default.aspx");
}
var httpHandler = BuildManager.CreateInstanceFromVirtualPath(path, typeof(IHttpHandler)) as IHttpHandler;
return httpHandler;
}
#endregion
}
Depois é só registar as rotas para as culturas pretendidas:
routes.Add("en", new Route("en/{*path}", new GlobalizationRouteHandler(CultureInfo.GetCultureInfo("en-US"))));
routes.Add("pt", new Route("pt/{*path}", new GlobalizationRouteHandler(CultureInfo.GetCultureInfo("pt-PT"))));
Hoje, estava a falar com o João acerca de um modo de emparelhar a duração da estado de sessão ASP.NET com a duração do ticket de autenticação por formulário.
A minha ideia era guardar o identificador de sessão na propriedade UserData forms authentication ticket durante o login e obtê-lo através de um session ID manager desenvolvido para o efeito.
O código do login seria algo assim:
protected void Login1_Authenticate(object sender, AuthenticateEventArgs e)
{
bool isPersistent = this.Login1.RememberMeSet;
string username = this.Login1.UserName;
var ticket = new FormsAuthenticationTicket(
0,
username,
DateTime.Now,
DateTime.Now.AddMinutes(2),
isPersistent,
Guid.NewGuid().ToString("N"));
// Encrypt the ticket.
var encryptedTicket = FormsAuthentication.Encrypt(ticket);
// Create the cookie.
this.Response.Cookies.Add(new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket));
// Redirect back to original URL.
this.Response.Redirect(FormsAuthentication.GetRedirectUrl(username, isPersistent));
}
Para efeitos de demonstração estou a usar um Guid como identificador de sessão.
O session ID manager vai retornar ao session state HTTP module o identificador de sessão uando este o pedir:
public class SessionIdManager : global::System.Web.SessionState.ISessionIDManager
{
#region ISessionIDManager Members
public string CreateSessionID(HttpContext context)
{
return GetDummySessionIdOrRedirectToLoginPage(context);
}
public string GetSessionID(HttpContext context)
{
return GetSessionIdFromFormsIdentity(context);
}
public void Initialize()
{
}
public bool InitializeRequest(HttpContext context, bool suppressAutoDetectRedirect, out bool supportSessionIDReissue)
{
supportSessionIDReissue = false;
return GetSessionIdFromFormsIdentity(context) == null;
}
public void RemoveSessionID(HttpContext context)
{
}
public void SaveSessionID(HttpContext context, string id, out bool redirected, out bool cookieAdded)
{
redirected = false;
cookieAdded = false;
}
public bool Validate(string id)
{
return true;
}
#endregion
private static string GetSessionIdFromFormsIdentity(HttpContext context)
{
var identity = context.User != null ? context.User.Identity as FormsIdentity : null;
if ((identity == null) || (identity.Ticket == null) || string.IsNullOrEmpty(identity.Ticket.UserData))
{
return GetDummySessionIdOrRedirectToLoginPage(context);
}
else
{
return identity.Ticket.UserData;
}
}
private static string GetDummySessionIdOrRedirectToLoginPage(HttpContext context)
{
if (context.Request.CurrentExecutionFilePath.Equals(FormsAuthentication.DefaultUrl, StringComparison.OrdinalIgnoreCase)
|| context.Request.CurrentExecutionFilePath.Equals(FormsAuthentication.LoginUrl, StringComparison.OrdinalIgnoreCase))
{
return Guid.NewGuid().ToString("N");
}
else
{
FormsAuthentication.RedirectToLoginPage();
return null;
}
}
}
NOTA: Embora este código deva funcionar, trata-se apenas de um exercício intelectual e não foi devidamente testado.
Hoje, o meu amigo Nuno estava a escrever algum código para obter as PropertyInfos da implementação de uma interface por parte de classe.
Dada este interface:
public interface ISomeInterface
{
int IntProperty { get; set; }
string StringProperty { get; }
void Method();
}
e esta classe:
public class SomeClass : ISomeInterface
{
int ISomeInterface.IntProperty { get; set; }
public int IntProperty { get; private set; }
public string StringProperty { get; private set; }
public void Method() { }
}
o Nuno queria obter:
- Int32 ISomeInterface.IntProperty
- System.String StringProperty
O código é relativamente simples. Primeiro é necessário obter os mapeamentos da interface:
typeof(SomeClass).GetInterfaceMap(typeof(ISomeInterface)).TargetMethods
e filtrar por PropertyInfos para as quais o MethodInfo é parte da sua implementação (implementa o método get ou o método set).
Algo como isto:
public static bool Implements(this MethodInfo methodInfo, PropertyInfo propertyInfo)
{
return (propertyInfo.GetGetMethod(true) == methodInfo) || (propertyInfo.GetSetMethod(true) == methodInfo);
}
Mas o que me chamou a atenção foi que, com os métodos de extensão acima, posso usar LINQ para obter as desejadas PropertyInfos.
Algo assim:
public static IEnumerable<PropertyInfo> GetInterfacePropertyImplementation(Type implementer, Type implemented)
{
return (from propertyInfo in implementer.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).AsEnumerable()
from methodInfo in implementer.GetInterfaceMap(implemented).TargetMethods.AsEnumerable()
where methodInfo.Implements(propertyInfo)
select propertyInfo).Distinct();
}
Para a classe e interface do exemplo, usar o método acimo seria algo como:
var q = GetInterfacePropertyImplementation(typeof(SomeClass), typeof(ISomeInterface));
foreach (var p in q)
{
Console.WriteLine(p);
}
O que produzirá o seguinte resultado:
Int32 ISomeInterface.IntProperty
System.String StringProperty
ACTUALIZADO: A implementação anterior era demasiado complexa e baseada em strings. Obrigado, Nuno.
No meu código faço uso extensivo de asserções de debugi (see System.Diagnostics.Debug.Assert). Estas asserções são muito uteis quando em debug porque deixa de ser necessário, uma a uma, percorrer todas as linhas de código para verificar que todas as pré-condições são satisfeitas. Assim que uma pré-condição falhar, é lançada uma janela com a informação da asserção e que nos permite abortar a execução (abort), ignorar e continuar (ignore) ou ir para a instrucção de asserção (retry).
Imaginem este código:
private void IKnowForSureThatANullStringWillNeverBePassed(string text)
{
System.Diagnostics.Debug.Assert(string != null, "text is null.");
// ...
}
Porque o método é privado, tenho todo o controlo dos valores que são passados no parâmetro text, estou a afirmar que nunca terá o valor null. Porque pode não ser óbvio que o valor de text nunca será null, a asserção funciona também como documentação.
Costumo correr os meus testes unitários e de integração em compilações de debug e estas asserções seriam muito úteis fazendo os testes falharem quando corro os meus testes unitários e de integração quando uma asserção falha em vez de continuar com a execução do método e falhar numa NullReferenceException. Foi por isso que eu (e mais pessoas) escrevi este simples TraceListener:
public class TraceListener : global::System.Diagnostics.TraceListener
{
public static readonly TraceListener Default = new TraceListener();
protected TraceListener()
{
this.Name = "Testing Trace Listener";
}
protected TraceListener(string name)
: base(name)
{
}
public override void Write(string message)
{
}
public override void WriteLine(string message)
{
}
public override void Fail(string message, string detailMessage)
{
var builder = new global::System.Text.StringBuilder();
builder.Append(message);
if (detailMessage != null)
{
builder.Append(" ");
builder.Append(detailMessage);
}
throw new global::Microsoft.VisualStudio.TestTools.UnitTesting.AssertFailedException(builder.ToString());
}
}
Este trace listener não escreve nada. Limita-se a disparar uma AssertFailedException quando é chamado o método Fail, que é o que acontece quando uma asserção falha.
Porque uma janela de falha de asserção não é desejável quando se estão a correr testes (especialmente se forem corridos de forma automática com parte de um processo de buil), o melhor é desabilitar a interface visual das asserções no ficheiro de configuração do projecto de testes.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.diagnostics>
<assert assertuienabled="false"/>
<trace>
<listeners>
<add name="TestTraceListener"
type="PauloMorgado.TestTools.VisualStudio.UnitTesting.Diagnostics.TraceListener, PauloMorgado.TestTools.VisualStudio" />
</listeners>
</trace>
</system.diagnostics>
</configuration>
Podem encontrar isto (e mais) em
PauloMorgado.TestTools no
CodePlex.
O Visual Studio usa a ferramenta Publicize para criar acessores públicos para membros e tipos privados de um determinado tipo.
Mas quando se tenta definir o valor de um elemento de um array privado de elementos de um tipo privado, a situação complica-se.
Imagine-se este hipotética classe a testar:
public static class MyClass
{
private static readonly MyInnerClass[] myArray = new MyInnerClass[10];
public static bool IsEmpty()
{
foreach (var item in myArray)
{
if ((item != null) && (!string.IsNullOrEmpty(item.Field)))
{
return false;
}
}
return true;
}
private class MyInnerClass
{
public string Field;
}
}
Se se quiser escrever um teste para o caso em que o array tem entradas “não vazias”, vai ser necessário primeiro inicializar o array.
Usando os acessores gerados pelo Visual Studio, o teste pode ser escrito desta froma:
[TestClass()]
public class MyClassTest
{
[TestMethod()]
public void IsEmpty_NotEmpty_ReturnsFalse()
{
for (int i = 0; i < 10; i++)
{
MyClass_Accessor.myArray[i] = new MyClass_Accessor.MyInnerClass { Field = i.ToString() };
}
bool expected = false;
bool actual;
actual = MyClass.IsEmpty();
Assert.AreEqual(expected, actual);
}
}
Mas o teste vai falhar porque, apesar dos elementos do array privado myArray poderem ser lidos como instâncias de MyClass_Accessor.MyInnerClass, não podem ser escritos como tal.
Para o fazer, o teste tem de ser escrito da seguinte forma:
[TestClass()]
public class MyClassTest
{
[TestMethod()]
public void IsEmpty_NotEmpty_ReturnsFalse()
{
for (int i = 0; i < 10; i++)
{
MyClass_Accessor.ShadowedType.SetStaticArrayElement("myArray", new MyClass_Accessor.MyInnerClass { Field = i.ToString() }.Target, i);
}
bool expected = false;
bool actual;
actual = MyClass.IsEmpty();
Assert.AreEqual(expected, actual);
}
}
Mas, deste modo, perdemos a característica fortemente tipada que nos dão os acessores gerados pelo Visual Studio porque é necessário escrever o nome do campo myArray.
Porque o acessor para o campo é uma propriedade, podem-se escrever alguns métodos de extensão para obter o nome do campo sem que seja necessário escrevê-lo como uma string. Algo assim:
public static class PrivateypeExtensions
{
public static void SetStaticArrayElement<T>(this PrivateType self, Expression<Func<T[]>> expression, T value, params int[] indices)
{
object elementValue = (value is BaseShadow) ? (value as BaseShadow).Target : value;
self.SetStaticArrayElement(
((PropertyInfo)((MemberExpression)(expression.Body)).Member).Name,
elementValue,
indices);
}
public static void SetStaticArrayElement<T>(this PrivateType self, Expression<Func<T[]>> expression, BindingFlags invokeAttr, T value, params int[] indices)
{
object elementValue = (value is BaseShadow) ? (value as BaseShadow).Target : value;
self.SetStaticArrayElement(
((PropertyInfo)((MemberExpression)(expression.Body)).Member).Name,
invokeAttr,
elementValue,
indices);
}
}
Sendo assim, o teste tomaria esta forma:
[TestClass()]
public class MyClassTest
{
[TestMethod()]
public void IsEmpty_NotEmpty_ReturnsFalse()
{
for (int i = 0; i < 10; i++)
{
MyClass_Accessor.ShadowedType.SetStaticArrayElement(() => MyClass_Accessor.myArray, new MyClass_Accessor.MyInnerClass { Field = i.ToString() }, i);
}
bool expected = false;
bool actual;
actual = MyClass.IsEmpty();
Assert.AreEqual(expected, actual);
}
}
Não é o mesmo que a primeria forma, mas é fortemente tipado e, se se mudar o nome ou o tipo do campo myArray, obterem-se um erro de compilação e não um erro de execução do teste.
Podem encontrar isto (e mais) em PauloMorgado.TestTools no CodePlex.
Finalmente consegui ter uma versão do Extended WebBrowser Control para colocar no CodePlex.
Ainda é um trabalho em curso, mas está usável (isto é uma palavra?). Estejam à vontade para descarregar, comentar e registra incidentes. Um tabbed web browserestá incluído como demonstração.
Depois de ter a minha aplicação .NET 1.1 a correr no ambiente de execução .NET 2.0 (^), comecei a planear migrá-la para .NET 2.0, mas faseadamente.
Porque não quero ter de manter 2 soluções (uma em Visual Studio 2003 para as assemblies .NET 1.1 e outra em Visual Studio 2008 para as assemblies .NET 2.0) decidi experimentar o MSBee e ter apenas uma solução Visual Studio 2008.
O MSBee é um projecto que esiste no CodePlex. Pode ser descarregado daí ou dos Microsoft Downloads. Porque a versão existente no Microsoft Downloads me pareceu ser a mais estável, foi a que descarreguei e instalei. O pacote contem um documento Word com as instruções de utilização.
Antes de se instalar o MSBee é necessário instalar o .NET 1.1 SDK.
Depois de tudo instalado, bastou abrir a solução Visual Studio 2003 no Visual Studio 2008 e deixá-lo convertê-la e aos projectos (quase 30).
Após a conversão, para compilar os projectos usando o compilador C# da plataforma .NET 1.1 C#, é necessário editar os ficheiros de de prjecto (.csproj) para sobrepor os targets po omissão peloas do MSBee adicionando o imports do MSBee a seguir aos da linguagem do projecto:
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Import Project="$(MSBuildExtensionsPath)\MSBee\MSBuildExtras.FX1_1.CSharp.targets" />
Outra alteração necessária (para Visual Studio 2008 - não sei se era necessária para Visual Studio 2005) é a versão das ferramentas. O MSBee necessita da versão 2.0. Para fazer esta alteração é ncessário alterar o atributo ToolsVersion no elemento raíz o ficheiro de projecto:
<Project DefaultTargets="Build" ToolsVersion="2.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
O MSBee tem a sua própria ideia acerca de onde coloca o resultado da compilação que não é a que eu tinha definido para os meus projectos. Há alguma informação acreca disto na documentação, mas decidi simplesmente comentar esta parte no ficheiro $(MSBuildExtensionsPath)\MSBee\MSBuildExtras.FX1_1.Common.targets:
<!-- Paulo
<Choose>
<When Condition=" '$(BaseFX1_1OutputPath)' == '' ">
<PropertyGroup>
<OutputPath>bin\FX1_1\</OutputPath>
</PropertyGroup>
</When>
<Otherwise>
<PropertyGroup>
<OutputPath>$(BaseFX1_1OutputPath)</OutputPath>
<OutputPath Condition=" !HasTrailingSlash('$(OutputPath)') ">$(OutputPath)\</OutputPath>
</PropertyGroup>
</Otherwise>
</Choose>
-->
<!-- Paulo
<PropertyGroup>
<BaseIntermediateOutputPath>obj\FX1_1\</BaseIntermediateOutputPath>
<IntermediateOutputPath Condition=" '$(PlatformName)' == 'AnyCPU' ">$(BaseIntermediateOutputPath)$(Configuration)\</IntermediateOutputPath>
<IntermediateOutputPath Condition=" '$(PlatformName)' != 'AnyCPU' ">$(BaseIntermediateOutputPath)$(PlatformName)\$(Configuration)\</IntermediateOutputPath>
<OutputPath Condition=" '$(PlatformName)' == 'AnyCPU' ">$(OutputPath)$(Configuration)\</OutputPath>
<OutputPath Condition=" '$(PlatformName)' != 'AnyCPU' ">$(OutputPath)$(PlatformName)\$(Configuration)\</OutputPath>
<- Once OutputPath is determined, set OutDir to its value. ->
<OutDir>$(OutputPath)</OutDir>
</PropertyGroup>
-->
Tudo parecia funcionar na minha elha máquina Windows XP sem qualquer plug-ins de terceiras partes para o Visual Studio, mas quando tentei na minha máquina Windows Vista X64, encontrei os seguintes problemas:
-
License Compiler
Porque estou a ausar controlso da Infragistics existe um ficheiro licences.licx que vai ser compilado. E isto costituiu um porblema.
O MSBee copia todos os ficheiros que necessita para o processo de compilação para uma pasta temporária, compila e depois copia o resultado para a localização configurada (output path).
O LC.exe parecia nunca ser capaz de encontrar as assemblies de que necessitava. Pesquisando, pareceu-me que era um problema antigo (desde os tempos da .NET 1.1) e a solução apontava para não compilar os ficheiros de licença. Por isso, comentei essa parte do ficheiro $(MSBuildExtensionsPath)\MSBee\MSBuildExtras.FX1_1.Common.targets:
<Target
Name="CompileLicxFiles" Condition="'@(_LicxFile)'!=''"
DependsOnTargets="$(CompileLicxFilesDependsOn)"
Inputs="$(MSBuildAllProjects);@(_LicxFile);@(ReferencePath);@(ReferenceDependencyPaths)"
Outputs="$(IntermediateOutputPath)$(TargetFileName).licenses">
<!--
<LC
Sources="@(_LicxFile)"
LicenseTarget="$(TargetFileName)"
OutputDirectory="$(IntermediateOutputPath)"
OutputLicense="$(IntermediateOutputPath)$(TargetFileName).licenses"
ReferencedAssemblies="@(ReferencePath);@(ReferenceDependencyPaths)"
ToolPath="$(TargetFrameworkSDKDirectory)bin\">
<Output TaskParameter="OutputLicense" ItemName="CompiledLicenseFile"/>
<Output TaskParameter="OutputLicense" ItemName="FileWrites"/>
</LC>
-->
</Target>
-
Resource Generator
Apesar de tudo fucionar bem em linha de comando, dentro do Visual Studio ocorria um erro na execução do ResGen.exe.
Consulanado o Windows Application Log descobri isto:
Faulting application Resgen.exe, version 1.1.4322.573, time stamp 0x3e559b5f, faulting module MockWeaver.dll, version 0.0.0.0, time stamp 0x4adb072e, exception code 0xc0000005, fault offset 0x00018fac, process id 0x4a50, application start time 0x01ca53c14488a2fb.
MockWeaver.dll pertence ao Isolator e basta-me inibi-lo quando compilo dentro do Visual Studio. Esperava começar a usar o Isolator neste projecto, mas, por enquanto, não vai ser possível.
Espero que isto possa ajudar alguém. Se precisarem de mais ajuda, provavelmente encontrá-la-ão nos foruns do MSBee no CodePlex.
O que interessa é: Não precisam do Visual Studio 2003!
Uma das aplicações que desenvolvo é uma aplicação Windows Forms .NET 1.1 usada por cerca de 5000 utilizadores e crítica para o negócio.
Sendo uma aplicação complexa e crítica, migrá-la para o ambiente de execução 2.0 apenas porque sim não seria uma opção porque significaria instalar um novo ambiente de execução e plataforma no ambiente estável dos postos de trabalho (Windows XP) e testar todas as aplicações.
Com o passar dos tempos, um elemento da equipa recebeu um novo posto de trabalho com o Windows Vista. Uma vez que apenas necessitava de .NET 2.0 para os seus desenvolvimentos, nunca instalou a .NET 1.1.
Um outro membro da equipa já tinha tentado migrar a aplicação para .NET 2.0 e deparou-se com os seguintes problemas:
-
O principal componente da aplicação é o
Web Browser Control. Este controlo extende o
AxHost, que, na transição da 1.1 para a 2.0 sofreu alterações e necessitava de profundas alterações para que pudesse ser compilado para a plataform 2.0.
-
Misturar chamadas síncronas e assíncronas não é permitido na plataforma 2.0 e a aplicação tinha, pelo menos, um caso desses na utilização de
HttpWebRequest/
HttpWebResponse.
A .NET 2.0 foi desenvolvida para ter o máximo de retro compatibilidade com a .NET 1.1 por forma a poder correr com o mínimo e impactos (nenhum, na maioria dos casos) aplicações desenvolvidas para a .NET 1.1. De facto, algumas das alterações passaram apenas por aplicar o atributo ObsoleteAttribute de modo a que o compilador dê erro, impedindo a compilação de novo código mas não impedindo a utilização por parte de código previamente compilado. Era este o caso do WebBrowserControl/AxHost e usar a assembly compilada para .NET 1.1, provavelmente, seria suficiente. E foi.
A questão do síncrono/assíncrono também foi fácil de resolver. Apenas requereu alterar isto:
request.GetRequestStream()
para isto:
request.EndGetRequestStream(response.BeginGetRequestStream(null, null))
E tudo funcionou como se estivesse a correr em .NET 1.1.
Mas ainda não é o fim da história. Mais tarde veio um requisito para que uma das páginas web que correm no controlo web browser usassem um componente ActiveX desenvolvido em .NET 2.0.
Por esta altura já os postos de trabalho tinham a plataforma 2.0 instalada.
Mas, como forçar a aplicação a correr no ambiente de execução 2.0 em vez de 1.1 dado que ambos estavam instalados nos postos de trabalho?
Tão simples como adicionar isto ao ficheiro de configuração (App.config):
<configuration>
<startup>
<requiredRuntime version="v2.0.50727" safemode="true"/>
</startup>
</configuration>
Tech Talks em Ferramentas de Desenvolvimento
4 Sessőes Webcast > Nivel 200/300
Microsoft Visual Studio Team System 2008: Test Edition
Uma sessăo dedicada á apresentaçăo da versăo de testes do Visual studio Team System 2008, especialmente das funcionalidades testes de carga, testes unitários e testes em cenários de internet.
Microsoft Visual Studio Team System 2008: Database Edition
Esta sessăo que abordará as funcionalidades que o Team System disponibiliza relativamente ao desenvolvimento de bases de dados, nomeadamente como se inserem no normal ciclo de desenvolvimento de software.
Visual Studio 2010 and .NET 4.0: Visual Studio Team System 2010 Part 1
As novas funcionalidades do Visual Studio Team System 2010, particularmente no que diz respeito ao Team Foundation Server, Relatórios e Gestăo de Projectos.
Visual Studio 2010 and .NET 4.0: Visual Studio Team System 2010 Part 2
As novas funcionalidades do Visual Studio Team System 2010 das ferramentas de suporte ao Programador e Arquitecto.
Orador:
Pedro Rosa
Developer Advisor
Microsoft Portugal
O LINQ trouxe-nos uma forma muito amigável de escrever consultas de forma independente do domínio das mesmas.
O facto de que o modo como as consultas são escritas é independente do domínio não quer dizer que todas vão ser compiladas e executadas do mesmo modo. É sempre necessário saber como o provedor se vai comportar.
O LINQ Para Objectos, por exemplo, vai compilar as consultas para chamadas a funções do tipo Func<> que retornam implementações de IEnumerable(T).
Por outro lado, o LINQ Para SQL vai compilar as consultas para uma árvore de expressões do tipo Expression<Func<>> e a sua execução retornará implementações de <IQueryable(T).
Porque as consultas LINQ Para SQL são compiladas para uma árvore de expressões, é possível que o provedor trate os elementos da árvore como bem entender.
Neste caso, isto quer dizer que todas as operações que poderem ser executadas na base de dados serão executadas na base de dados e o programador(a) tem de ter noção disto quando escrever as consultas.
Consideremos um exemplo usando a base de dados AdventureWorks (se não tiverem, podem descarregar daqui).
Eu quero construír uma lista de saudações para todos os empregados que tenham a marcaSalariedFlag, na forma:
[Mr.|Mrs.|Miss] <first name> <middle name> <last name>
Mas há um pequeno detalhe na base de dados: FirstName, MiddleName e LastName podem ter espaços no fim e eu não os quero.
É algo tão simples como isto:
var q1 = from e in context.Employees
where e.SalariedFlag
select
((e.Gender == 'F') ? ((e.MaritalStatus == 'S') ? "Miss" : "Mrs.") : "Mr.") + " " +
e.Person.FirstName.Trim() +
(e.Person.MiddleName == null || e.Person.MiddleName.Trim().Length == 0 ? " " : " " + e.Person.MiddleName.Trim() + " ") +
e.Person.LastName.Trim();
que será executado na base de dados como:
SELECT ((((
(CASE
WHEN UNICODE([t0].[Gender]) = @p0 THEN
(CASE
WHEN UNICODE([t0].[MaritalStatus]) = @p1 THEN @p2
ELSE @p3
END)
ELSE CONVERT(NVarChar(4),@p4)
END)) + @p5) + LTRIM(RTRIM([t1].[FirstName]))) + (
(CASE
WHEN ([t1].[MiddleName] IS NULL) OR (LEN(LTRIM(RTRIM([t1].[MiddleName]))) = @p6) THEN CONVERT(NVarChar(MAX),@p7)
ELSE (@p8 + LTRIM(RTRIM([t1].[MiddleName]))) + @p9
END))) + LTRIM(RTRIM([t1].[LastName])) AS [value]
FROM [HumanResources].[Employee] AS [t0]
INNER JOIN [Person].[Person] AS [t1] ON [t1].[BusinessEntityID] = [t0].[BusinessEntityID]
WHERE [t0].[SalariedFlag] = 1
-- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) [70]
-- @p1: Input Int (Size = 0; Prec = 0; Scale = 0) [83]
-- @p2: Input NVarChar (Size = 4; Prec = 0; Scale = 0) [Miss]
-- @p3: Input NVarChar (Size = 4; Prec = 0; Scale = 0) [Mrs.]
-- @p4: Input NVarChar (Size = 3; Prec = 0; Scale = 0) [Mr.]
-- @p5: Input NVarChar (Size = 1; Prec = 0; Scale = 0) [ ]
-- @p6: Input Int (Size = 0; Prec = 0; Scale = 0) [0]
-- @p7: Input NVarChar (Size = 1; Prec = 0; Scale = 0) [ ]
-- @p8: Input NVarChar (Size = 1; Prec = 0; Scale = 0) [ ]
-- @p9: Input NVarChar (Size = 1; Prec = 0; Scale = 0) [ ]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.4926
Como podem ver, há um elevado número de operações sobre texto a serem feitas, na base de dados, por cada linha retornada.
Dependendo do número de linhas ou da carga na base de dados isto pode ser muito mau. Pode inclusive resultar num timeout.
Sendo assim, como é que forçamos as operações de texto a ocorrer no cliente em vez da base de dados?
Apenas os IQueryable<T> serão traduzidos para T-SQL. Por isso, tudo o que é necessário fazer é mudar o tipo de enumerador a iterar.
Uma forma de o fazer é usar o método AsEnumerable da classe Enumerable.
A nova consulta será escrita assim:
var q2 = from e in context.Employees.Where(e => e.SalariedFlag).AsEnumerable()
select
((e.Gender == 'F') ? ((e.MaritalStatus == 'S') ? "Miss" : "Mrs.") : "Mr.") + " " + e.Person.FirstName.Trim() +
(e.Person.MiddleName == null || e.Person.MiddleName.Trim().Length == 0 ? " " : " " + e.Person.MiddleName.Trim() + " ") +
e.Person.LastName.Trim();
e executada na base de dados como:
SELECT
[t0].[BusinessEntityID],
[t0].[LoginID],
[t0].[NationalIDNumber],
[t0].[JobTitle],
[t0].[MaritalStatus],
[t0].[BirthDate],
[t0].[Gender],
[t0].[HireDate],
[t0].[SalariedFlag],
[t0].[VacationHours],
[t0].[SickLeaveHours],
[t0].[CurrentFlag],
[t0].[rowguid],
[t0].[ModifiedDate],
[t1].[BusinessEntityID] AS [BusinessEntityID2],
[t1].[PersonType],
[t1].[NameStyle],
[t1].[Title],
[t1].[FirstName],
[t1].[MiddleName],
[t1].[LastName],
[t1].[Suffix],
[t1].[EmailPromotion],
[t1].[AdditionalContactInfo],
[t1].[Demographics],
[t1].[rowguid] AS [rowguid2],
[t1].[ModifiedDate] AS [ModifiedDate2]
FROM [HumanResources].[Employee] AS [t0]
INNER JOIN [Person].[Person] AS [t1] ON [t1].[BusinessEntityID] = [t0].[BusinessEntityID]
WHERE [t0].[SalariedFlag] = 1
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.4926
Como podem notar, as operações de texto já não são executadas na base de dados mas, em contrapartida, todas as colunas de ambas as tabelas estão a ser retornadas. E isto continua a ser mau porque está a ser consumida largura de banda desnecessáriamente.
A forma de escolher as columas que serão retornadas na consulta é seleccionar apenas as colunas pretendidas. Mas porque continuamos a querer que as operações sobre texto sejam executadas no cliente, temos de projectar essas colunas num objecto intremédio. Prque não necessitamos desse objecto fora da consulta, usaremos um tipo anónimo.
A consulta será agora escrita assim:
var q3 = from n in
(
from e in context.Employees
where e.SalariedFlag
select new
{
Gender = e.Gender,
MaritalStatus = e.MaritalStatus,
FirstName = e.Person.FirstName,
MiddleName = e.Person.MiddleName,
LastName = e.Person.LastName
}
).AsEnumerable()
select ((n.Gender == 'F') ? ((n.MaritalStatus == 'S') ? "Miss" : "Mrs.") : "Mr.") + " " + n.FirstName.Trim()
+ (n.MiddleName == null || n.MiddleName.Trim().Length == 0 ? " " : " " + n.MiddleName.Trim() + " ")
+ n.LastName.Trim();
e executada na base de dados como:
SELECT
[t0].[Gender],
[t0].[MaritalStatus],
[t1].[FirstName],
[t1].[MiddleName],
[t1].[LastName]
FROM [HumanResources].[Employee] AS [t0]
INNER JOIN [Person].[Person] AS [t1] ON [t1].[BusinessEntityID] = [t0].[BusinessEntityID]
WHERE [t0].[SalariedFlag] = 1
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.4926
Note-se que a chamada a Enumerable.AsEnumerable para traduzir de LINQ Para SQL para LINQ Para Objectos.
E, para terminar, Se não se usarem operações de texto na consulta, estas não serão, obviamente, traduzidas para T-SQL:
var q4 = from e in context.Employees
where e.SalariedFlag
select BuildSalutation(e.Gender, e.MaritalStatus, e.Person.FirstName, e.Person.MiddleName, e.Person.LastName);
em que BuildSalutation é implementado como:
private static object BuildSalutation(char gender, char maritalStatus, string firstName, string middleName, string lastName)
{
return ((gender == 'F') ? ((maritalStatus == 'S') ? "Miss" : "Mrs.") : "Mr.") + " "
+ firstName.Trim()
+ (middleName == null || middleName.Trim().Length == 0 ? " " : " " + middleName.Trim() + " ")
+ lastName.Trim();
}
e executado na base de dados como:
SELECT
[t0].[Gender] AS [gender],
[t0].[MaritalStatus] AS [maritalStatus],
[t1].[FirstName] AS [firstName],
[t1].[MiddleName] AS [middleName],
[t1].[LastName] AS [lastName]
FROM [HumanResources].[Employee] AS [t0]
INNER JOIN [Person].[Person] AS [t1] ON [t1].[BusinessEntityID] = [t0].[BusinessEntityID]
WHERE [t0].[SalariedFlag] = 1
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.4926
É de notar que a consulta T-SQL gerada é praticamente a mesma que no caso anterior.
Se ainda está a ler. espero que tenha ficado com a noção de que, a forma como escrever as consultas LINQ Para SQL afecta o T-SQL gerado.
Hoje, dia 23 de Setembro, quarta-feira, pelas 20h00 no auditório da Microsoft irá ser feito o PréRemix.
Consistirá na apresentação das mesmas duas sessões focadas na Cloud que serão apresentadas no Remix.
Cloud computing: A plataforma Azure
Luis Martins e Pedro Félix
Saiba como construir aplicações na cloud ou usar serviços interoperaveis sobre a infraestrutura Microsoft. Venha aprender como poderá tirar partido desta plataforma para solucionar os desafios de desenvolvimento de software. Nesta sessão poderá ainda ouvir os conceitos essenciais do Windows Azure, o que há de novo e como se posiciona face à concorrência.
Armazenamento na cloud
José António Silva
A escalabilidade do storage implica tipicamente um conjunto de decisões de arquitectura, de forma a manter afastadas as limitações impostas pelos sistemas de gestão de base de dados relacionais. Estas decisões não são mais opcionais neste entusiasmante mundo dos cloud services.
A boa notícia diz-nos que, assim que começamos a desenvolver para os novos storage engines, o mais provável é que fiquemos viciados nestes modelos mais flexíveis.
Junte-se a nós nesta sessão e fique a conhecer as tables, queues, key-values e outros patterns que nos ajudam a criar camadas de dados persistentes escaláveis e distribuídas para a cloud.


Pelo valioso contributo que têm trazido à discussão e partilha de boas práticas sobre a utilização da plataforma .NET, a Microsoft Portugal está a oferecer, aos membros da comunidade pontoNETpt, vouchers com 20% de desconto sobre o preço do registo no REMIX.
Quem estiver interessado em fazer uso deste benefício deverá enviar um e-mail para cristina.carvalho at microsoft.com com pedido de voucher-desconto. O e-mail deverá ter como assunto pontoNETpt no ReMIX e incluír no seu corpo o endereço do seu perfil na comunidade (http://pontonetpt.com/members/<membro>/).
Actualização: Para quem já fez o pagamento, e não podendo fazer retorno do investimento, a Microsoft tem o maior gosto em oferecer um voucher com 50% de desconto para oferecer a quem considere ter interesse nos conteúdos deste evento.
Mais Entradas
Página seguinte »