Janeiro 2010 - Posts
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.