Introduction
It's better to familiarize yourself with new technologies by creating a prototype using the basic architectural patterns. For complex applications, it is:
- dividing into logical layers
- user authorization
- exception handling and auditing
- resource localization
- unit testing
and many others. But for the first time, it's quite enough.
The example presented in the article uses WCF RIA Services Class Library to group Business Logic components into a separate project. It turned out to be a not so trivial task, the more so because most examples in the WCF RIA Services Code Gallery do not use this library - nothing to crib :)
These samples were a good launch pad, but I needed a bit different code. First, I wanted to use my own database for user authorization. Besides, I decided to use LINQ to SQL for my Data Access Layer (DAL) and implement the Repository pattern to provide dependency-free access to my data and facilitate unit testing.
And finally, resource localization itself was not a challenging task, but required some handiwork.
This is what we shall see when the example is built:
Requisites
But before we build the application, I would like to enumerate what was used during my work with the project:
- Microsoft Visual Studio 2008 SP1
- Microsoft Silverlight 3
- Microsoft Silverlight 3 SDK
- Microsoft Silverlight 3 Toolkit November 2009
- Microsoft Silverlight 3 Tools for Visual Studio 2008 SP1
- WCF RIA Services Beta
- SQL Server 2008 Express
Project structure
The solution contains the following projects:
BizApp.Web |
Web project hosting this Silverlight application. It is our server project. |
BizApp |
Silverlight client application. |
BizApp.Controls |
Auxiliary project, containing UI controls. |
BizApp.Services.Server |
WCF RIA Services Class Library. Server-tier project. |
BizApp.Services |
WCF RIA Services Class Library. Client-tier project. Contains generated code; visible if you click the "Show All Files" icon. |
BizApp.Services.Test |
Unit tests. |
Repositories and unit testing
The Repository pattern is used to encapsulate data access methods in an abstract layer. That gives a possibility to create mock objects, implementing the IRepository
interface, and using them in unit tests, not touching a real database.
I took the Testing Domain Services example and Vijay's article Unit Testing Business Logic in .NET RIA Services as a basis. The only thing left to do was to initialize repository classes, not using hard-coding. For that purpose, I used dependency injection implemented with the Microsoft Unity Application Block 1.2. I think that requires some explanation.
First of all, my services (AuthenticationService
, UserRegistrationService
) use the following properties, decorated with the Dependency
attribute, for access to data repositories:
[Dependency]
public IRepository<UserAccount> UserAccountRepository { get; set; }
[Dependency]
public IRepository<UserRole> UserRoleRepository { get; set; }
But, how does the services initialize these properties? That happens during the creation of a service in the DomainServiceFactory
class:
public DomainService CreateDomainService(Type domainServiceType,
DomainServiceContext context)
{
var domainService =
DependencyInjectionProvider.UnityContainer.Resolve(
domainServiceType) as DomainService;
domainService.Initialize(context);
return domainService;
}
You do not need to call this method explicitly. All you need is just to initialize our DomainServiceFactory
in the Global.asax.cs file:
protected void Application_Start(object sender, EventArgs e)
{
AppDatabaseDataContext dataContext = new AppDatabaseDataContext();
DependencyInjectionProvider.Configure(
(System.Data.Linq.DataContext)dataContext);
DomainService.Factory = new DomainServiceFactory();
}
Coming back to the CreateDomainService()
method, I would like to note that the UnityContainer
resolves encountered dependencies using our instructions in the unity.config file. Particularly, the IRepository<UserAccount>
interface is mapped to the LinqToSqlRepository<UserAccount>
class.
But, if you look at the class, you will see that it can be instantiated using a public constructor with a parameter of type DataContext
. The trick is that the UnityContainer
already knows about it and will use AppDatabaseDataContext
to initialize the LinqToSqlRepository<UserAccount>
object (see the DependencyInjectionProvider.Configure()
method). Thereby, our repositories are configured to use AppDatabaseDataContext
generated from the database.
Now, we can test our services, replacing the repositories with mock objects. Open the BizApp.Services.Test project containing the unit tests. Each of them initializes testing services with mock repositories, implementing the IRepository<T>
interface. A more detailed description can be found in Vijay's blog.
User authorization
This example uses Forms authentication mode, i.e., a user enters his or her login name and password and the program tries to identify the user browsing through the list of user accounts in the AppDatabase.mdf database. Authentication logic is implemented in the AuthenticationService
class exposing the IAuthentication<User>
interface. To enable authentication, we have to specify the domain context generated from our authentication domain service in the App.xaml file:
<Application.ApplicationLifetimeObjects>
<app:WebContext>
<app:WebContext.Authentication>
<appsvc:FormsAuthentication
DomainContextType=
"BizApp.Services.Server.DomainServices.AuthenticationContext,
BizApp.Services, Version= 1.0.0.0"/>
</app:WebContext.Authentication>
</app:WebContext>
</Application.ApplicationLifetimeObjects>
The main function of the service is the Login()
method. And, the key point of the method is the statement:
FormsAuthentication.SetAuthCookie(userName, isPersistent);
MSDN states that it adds a forms-authentication ticket to either the cookies collection or the URL, and the ticket supplies forms-authentication information to the next request made by the browser. In other words, you do not need to make an effort to identify a user - WCF RIA Services does it for you. All that you need is stored in the ServiceContext.User.Identity
property. Or, you can call the GetUser()
method to get an authenticated user.
For the more inquisitive developer, I would recommend to install Fiddler and WCF Binary-encoded Message Inspector for Fiddler and try to capture HTTP traffic between the Silverlight application and your server. But, be aware of one drawback - Fiddler cannot capture local traffic (see troubleshooting). To get around this, just place a dot after localhost in the browser:
http://localhost.:3121/BizApp.aspx
Start Fiddler. Run the application, create a user account, and login. Choose an authentication request sent by the application in Fiddler. If you look at the server response, you will see authorization cookies like that:
The forms authentication ticket, stored in the cookies, is encrypted and signed using the machine's key. So, this information can be considered as secure. More detailed information about forms authentication cookies can be found here.
Exception handling
Sooner or later, you will have to design your exception-management strategy. WCF RIA Services provides a way to handle exceptions occurred on the server-side in the DomainService.OnError
method. To perform exception logging, I inherited my services from the DomainServiceBase
class with an overridden OnError
method. It helps to catch exceptions at layer boundaries:
protected override void OnError(DomainServiceErrorInfo errorInfo)
{
if (errorInfo != null && errorInfo.Error != null)
{
EventLog eventLog = new EventLog("Application");
eventLog.Source = "BizApp";
eventLog.WriteEntry(errorInfo.Error.Message, EventLogEntryType.Error);
}
base.OnError(errorInfo);
}
Localized resources
If you run the example, you will notice a drop-down list for language selection:
There are two places in the application containing some text that can be localized. First, the static text displayed on labels, buttons, checkboxes, and so on. Second - text resources used for data annotation on the Service Layer.
Localized text on the UI
To localize UI elements, we shall create resource files for the chosen languages:
Besides, we have to perform the following steps:
- Set "Custom Tool" to
PublicResXFileCodeGenerator
for the default resource only. It is LocalizedStrings.resx in our case.
- Set "Access Modifier" to
Public
for the default resource file
- Specify
SupportedCultures
in the .csproj file.
Visual Studio does not provide a way to set SupportedCultures
for a project. So, we have to edit our BizApp.csproj file manually. Open it in an editor, and add a tag with the languages you are going to use:
<SupportedCultures>de-de;es-es;fr-fr</SupportedCultures>
I created the ApplicationResources
class containing the LocalizedStrings
property. Then, I made it available for the whole application, adding it to application resources in the App.xaml file:
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Assets/Styles.xaml"/>
<ResourceDictionary>
<res:ApplicationResources x:Key="LocalStrings" />
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
Now, we can use our localized strings in XAML:
<Button Content="{Binding Source={StaticResource LocalStrings},
Path=LocalizedStrings.LoginButton}"
Click="Login_Click" Width="75" Height="23" />
To change the UI language, we have to change the culture in the current thread and reset the resources (see AppMenu.xaml.cs):
private void Language_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (cbxLanguage != null)
{
ComboBoxItem item = cbxLanguage.SelectedItem as ComboBoxItem;
Thread.CurrentThread.CurrentCulture =
new CultureInfo(item.Content.ToString());
Thread.CurrentThread.CurrentUICulture =
new CultureInfo(item.Content.ToString());
((ApplicationResources)Application.Current.
Resources["LocalStrings"]).LocalizedStrings =
new BizApp.Resources.LocalizedStrings();
}
}
Localized text on the Service Layer
The System.ComponentModel.DataAnnotations
namespace provides a convenient way to decorate our data with metadata for their validation (for example, see RegistrationData.cs):
[Display(Name = "FullNameLabel", Description = "FullNameDescription",
ResourceType = typeof(RegistrationDataResources))]
[StringLength(100, ErrorMessageResourceName = "ValidationErrorBadFullNameLength",
ErrorMessageResourceType = typeof(ErrorResources))]
public string FullName { get; set; }
Messages are stored in resource files and can be localized. It's not a problem to create resource files - it's a problem to get it compiled if you use the WCF RIA Services Class Library. The BizApp.Services project contains some code, generated from classes defined in the BizApp.Services.Server project. But some of them can contain references to resources not existing in the BizApp.Services project. I'll show you how to overcome it.
First of all, resources in the BizApp.Services.Server project shall have the Public
access modifier. Then, we have to create a folder Server\Resources in the BizApp.Services project. Note that the folder structure must match the resource file namespace in the BizApp.Services.Server project!
The next step is to add resources to BizApp.Services. Select the Server\Resources folder and bring up the Add Existing Item... dialog. Select the *Resources.resx and *Resources.Designer.cs files and add them as link files to the BizApp.Services project. Save the project and unload it from the Solution. Open BizApp.Services.csproj in an editor and find sections with our *Resources.Designer.cs files:
<Compile Include="..\BizApp.Services.Server\Resources\ErrorResources.Designer.cs">
Add <AutoGen>
, <DesignTime>
, and <DependentUpon>
sub-sections. The final result shall look like the following:
<Compile Include="..\BizApp.Services.Server\Resources\ErrorResources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>ErrorResources.resx</DependentUpon>
<Link>Server\Resources\ErrorResources.Designer.cs</Link>
</Compile>
Now, you can reload the project in Visual Studio, build it, and run.
Conclusion
My article does not claim to newness or completeness. It's rather a collection of recipes or how-to's found during my attempt to build this application. Below, you will find some sources of my wisdom :)
References
History
- 3 March, 2010: Initial post.