The Ed-Fi “Classic Dashboards” are no longer supported through the Ed-Fi Alliance. You can still receive support and maintenance through the Ed-Fi vendor community. Please look at any of the vendors’ dashboard solutions on the Registry of Ed-Fi Badges or the Ed-Fi Starter Kits if you are looking for a visualization solution to use with the Ed-Fi ODS. This documentation will remain available to assist existing Classic Dashboard implementers.

UI Developers' Guide - Architectural Pattern Overview

This section provides a technical description of the major design and architecture patterns for the Ed-Fi Dashboards web application. The information in this section provides information on the overall architecture and major design patterns used in the Dashboards so that an accomplished developer in the technology can successfully navigate and understand the software.

The Ed-Fi Dashboards platform architecture uses a collection of design patterns and software architecture best-practices. In order to understand the code base, it is important to understand these patterns and techniques. This section provides a brief introduction to the most pertinent topics, and there are many well-written books covering these patterns and practices in more detail. 

Loosely Coupled Software Design

With the rise in popularity of object-oriented software in the 1990's, the predominant approach to software design was to create deep hierarchical models of inheritance. A prime example of this approach is seen in Microsoft’s MFC (Microsoft Foundation Classes).

A problem with this style of architecture (also known as “extension by inheritance”) is that classes end up with dependencies on many other classes, and changing behavior in a base class may yield unwanted results in a distant class. Individual classes often cannot be used independently of their original purpose because they are inextricably linked to the entire hierarchy. While arguably appropriate for some scenarios, deep inheritance hierarchies can become an obstacle to reuse and to change – and in agile software organizations like the Ed-Fi licensees, change is a reality.

The Ed-Fi Dashboards application has instead been designed with an approach that favors “extension by composition” using a technique known as dependency injection.

Dependency Injection

With dependency injection, the behaviors a class needs are provided through a wholly self-contained external component rather than through an inheritance relationship from a base class. Additionally, the class does not depend directly on the external component, but rather an abstraction of that component (an interface in C#) and therefore can easily be configured to use completely different implementations for different scenarios. For example, the dashboards support a feature to show images for the students. During development and public demonstrations, there are be privacy concerns with showing real student images, so random student images from a library of stock photos are shown. In production, however, real student images (if available) are shown, but those images might come from a file system or a binary database column. All of these implementations are provided through the abstraction that is the IImageContentProvider interface.

Inversion of Control (IoC)

Inversion of Control (IoC) containers are software components that are responsible for the composition of software at runtime. In the .NET space, there are many options available such as Unity (Microsoft), Managed Extensibility Framework (Microsoft), NInject, StructureMap, and Spring.NET. The dashboard application uses Castle Windsor from the Castle open source project. In this documentation, the term “container” refers to the Castle Windsor IoC container.

IoC Usage Scenario

Typically in a C# project, developers will handle the runtime creation of all their classes using the new keyword. In a project using an IoC container, the container is made aware of all the components in the system that it will need to create. Then, on demand, it can instantiate and provide a reference to the appropriate objects.

For example, when creating the StaffController for the web application, the system flow will look something like this:

  1. A secure HTTP request arrives at the web server and after a bit of processing is matched with a route defined by the dashboard MVC application.
  2. Using the incoming route information, the controller factory is queried for the controller type.
  3. ASP.NET MVC uses the dependency resolver to get an instance of the controller. The Ed-Fi Dashboards implementation of the dependency resolver asks the already-configured Castle Windsor container for an instance of the controller, based on its type.
  4. The container inspects the controller’s constructor and sees that it takes a dependency on the IStaffService service:

    public class StaffController : Controller
    {
        private readonly IStaffService service;
    
        public StaffController(IStaffService service)
        {
            this.service = service;
        }
        ...
  5. The container inspects its component registrations for the type we previously registered as the implementation of the IStaffService service. In the core version of the dashboard system, this is the StaffService class:

    public class StaffService : IStaffService
    {
        private readonly IRepository<StaffInformation> staffInformationRepository;
        private readonly IRepository<StaffEducationOrgInformation> staffEducationOrgInformationRepository;
        private readonly IStaffAreaLinks staffLinks;
    
        public StaffService(IRepository<StaffInformation> staffInformationRepository, IRepository<StaffEducationOrgInformation> staffEducationOrgInformationRepository, 
        IStaffAreaLinks staffLinks)
        {
            this.staffInformationRepository = staffInformationRepository;
            this.staffEducationOrgInformationRepository = staffEducationOrgInformationRepository;
            this.staffLinks = staffLinks;
        }
    
  6. The container inspects the StaffService constructor and sees that it needs instances of the IRepository<StaffInformation>, IRepository<StaffEduationOrgInformation>, and IStaffAreaLinks. Once again, it searches its registrations for implementations of the specified types, and repeats the process until there are no more dependencies to resolve.
  7. With the entire cascade of dependencies needed to successfully create the StaffController now identified, the container begins constructing and injecting objects as necessary, and returns a fully initialized StaffController instance. All of this compositional behavior happens automatically without a single new statement.

Single Component Registration

For many components, there should only ever be one implementation registered with the IoC container. In the Ed-Fi Dashboards application, this registration occurs in the ConfigurationSpecificInstallerBase and derived classes (where the registration needs to be different for a particular environment). The following code snippet shows the basic format of the registration methods.

protected virtual void RegisterIMetricRenderingEngine(IWindsorContainer container)
{
     container.Register(Component
         .For<IMetricRenderingEngine>()
         .ImplementedBy<MetricRenderingEngine>());
}

This registration essentially tells the container to create an instance of the MetricRenderingEngine class whenever a class needs an IMetricRenderingEngine instance.

Multiple Component Registration

For other components in the system, there are entire categories of components that need to be registered. For example, to serve the needs of the ASP.NET MVC application, all the controllers will need to be registered with the container. Rather than explicitly registering each individual class, the Ed-Fi Dashboards application implements installers. The following code snippet shows the controller installer.

/// <summary>
/// Registers the controller classes (for the Model-View-Controller pattern) with
/// the Castle Windsor Inversion of Control container.
/// </summary>
public class ControllerInstaller<TMarker> : IWindsorInstaller
{
    public void Install(IWindsorContainer container, IConfigurationStore store)
    {
        // Register all controllers
        container.Register(
            AllTypes
                .FromAssemblyContaining<TMarker>()
                .Where(t => typeof(IController).IsAssignableFrom(t))
                .Configure(c => c.LifeStyle.Transient)
        );
    }
}

Note that this installer also sets the lifestyle of the instances as “Transient”. By default, Castle Windsor provides components with a “singleton” lifestyle which means there is only one instance of the class serving all requests. Singleton lifestyles are appropriate for components that don’t have instance-specific state. However, since controllers exist to serve individual HTTP requests, the application needs new instances of the controllers created each time one is requested.

Aspect-Oriented Programming (AOP)

Aspect-Oriented Programming is a programming technique enabled by the user of an IoC container that supports the implementation of cross-cutting concerns. A cross-cutting concern is a feature that would apply to all (or many) components of a system. Examples of cross-cutting concerns would include logging, caching, security, and global exception handling. Including the logic for these features in every class would be time consuming and involve a huge amount of redundancy.

Aspects are components that implement cross-cutting concerns. The “Hello World” sample for an aspect is usually a logging aspect which, when activated, is capable of logging before and after events of all class method invocations. In Castle Windsor, aspects are implemented via the IInterceptor interface. The Ed-Fi Dashboards application uses interceptors for providing service layer claims-based authorization, caching, and filtering.

Patterns

This section briefly introduces the key software architecture patterns that are used in the Ed-Fi Dashboards application.

Model/View/Controller (ASP.NET MVC)

The Model/View/Controller pattern lends itself particularly well to web development. The Ed-Fi Dashboards implement the MVC pattern using the ASP.NET MVC framework. When an HTTP request is received by the web server, it is matched (using predefined patterns) to a route which in turn is resolved to a controller. The controller then typically interacts with a database or service to prepare a view model which is then sent to a view for rendering.

Some of the default behavior of the framework has been customized for the specific needs of the Ed-Fi Dashboards application, and it is useful to have some knowledge and experience with ASP.NET MVC to aid in understanding the application’s source code.

Models

In the Ed-Fi Dashboards application, models are used for transferring data from the services to their corresponding views. They are data-centric view model classes, not rich domain models, and thus they lack behavior.

Each service method returns a single model object that represents the "Response" class of the Request/Response pattern (described below).  Many of these response models will actually be complete object graphs. The name for the class representing the outer-most object in the object graph should be suffixed with the text "Model" to indicate its role as a response model. Since the response model is purpose-built for the view/resource it serves, the classes that represent the data contained in the object graph should generally be defined as internal classes within the model class, and the names for these internal classes should not be suffixed with "Model".

Services (Request/Response)

In the Ed-Fi Dashboards application, services essentially function as HTTP resource handlers. The services are designed around a request/response pattern. Each service exposes a single method named Get (to reflect the handling of an HTTP GET request), accepting a distinct type of "request" model and returning a distinct "response" model. This approach enables the exposure of the service methods as HTTP resources which are addressable via routes. While only currently experimental, the data for each resource can be retrieved as JSON by adding a format=json parameter to the request, or by including an accept header on the HTTP request with a value of application/json.

Additionally, by keeping the services separate from the application, it provides a layer upon which to apply authorization on the method invocations using the AOP (Aspect-Oriented Programming) techniques described earlier.

Generic Repository

Repositories provide access to the data in a decoupled manner that enables unit testing of the services independent of a physical database. The interface for the generic repository provides a single method (GetAll) which returns an IQueryable<T>, where T represents a data entity for an underlying database table. A service will typically take constructor dependencies on several repository classes (one for each database table that is needed), and then use LINQ to execute queries against the tables. In the Ed-Fi Dashboards application, a customized version of the SubSonic SimpleRepository class is used as the underlying LINQ provider for generating the appropriate SQL for execution against Microsoft SQL Server.

Providers

Providers are treated as the main extensibility points for the system. A provider interface may have multiple implementations in existence of which only one would be configured for use within the application at a time. For example, the ICacheProvider interface defines methods for caching data and the current Ed-Fi Dashboards application only provides the AspNetCacheProvider class which uses the cache provided by ASP.NET. It is conceivable that for another environment, an implementation backed by a distributed cache such as Microsoft’s Velocity would be more appropriate. While they would be different implementations of the provider, it is not likely they would be used simultaneously. That is the essence of what the Ed-Fi architecture considers to be a “provider”.

Chain of Responsibility

In some cases, the system may define a provider interface for a piece of functionality, but for extensibility reasons the implementation is not satisfied by a single class but a series of classes, each of which handle a particular scenario.

In a chain of responsibility scenario, the application is provided with a single provider implementation which it calls to get the image it needs. However, each implementation of the provider also implements the chain of responsibility behavior of calling the next class in the chain if it cannot handle the request. In this way every implementation gets a chance of handling the request until one does. The calling application has no knowledge that more than one class was involved in servicing the request.

For example, the IImageContentProvider interface defines a contract for integrating domain-specific images into the application (such as students, staff, schools, and local education agencies).

/// <summary>
/// Gets the binary contents of an image.
/// </summary>
public interface IImageContentProvider
{
    /// <summary>
    /// Gets the binary contents of an image, as specified by the request.
    /// </summary>
    /// <param name="request">A concrete request containing the information necessary to identify and return the correct image.</param>
    /// <returns>A model containing the image contents and content type.</returns>
    ImageModel GetImage(ImageRequestBase request);
}

However, depending on the environment and/or the image management strategy, the images loaded might be randomized stock photos (e.g., for demonstrations), real yearbook pictures supplied by a local education agency, or a default generic image when no pictures are available. Additionally, the images could be located on a file system or in database columns, or some combination thereof. Clearly, a single class implementing all this behavior would be inappropriate.

By registering a chain of responsibility with the container, the flexibility of a compositional architecture continues beyond the provider abstraction. The Ed-Fi Dashboards application provides a ChainOfReponsibilityRegistrar class to assist with registering chain of responsibility implementations with the container, and a ChainOfResponsibilityBase class containing the behavior for members of a chain to derive from.