Partager via


Appendix A - The Contoso Microsoft Azure Media Services Web Service

patterns & practices Developer CenterDownload Reference ImplementationDownload book

The purpose of the Contoso web service is to take REST requests sent from the client applications, validate these requests by applying the appropriate business logic, and then convert them into the corresponding Media Services operations. This appendix describes how the Contoso web service was designed and implemented to perform these tasks.

Understanding the web service

The developers at Contoso need the web service to provide the functionality to support the core business cases described in Chapter 2, "The Azure Media Services Video-on-Demand Scenario" The CMS uses an SQL database, but the developers wanted to design a system that decoupled the data storage technologies from the business logic of the web service. This approach enables Contoso to switch a particular repository to use a different data storage technology if necessary, without impacting the business logic in the web service. It also helps to ensure that the low level details of the database are not visible to the client applications, which could result in accidental dependencies between the user interface and the database.

The Contoso web service is implemented in the Contoso.Api project in the solution provided with this guide.

The web service exposes its functionality through a series of controllers that respond to HTTP REST requests. The section "Routing incoming requests to a controller" describes the controllers and the requests that they support.

The business logic inside each controller stores and retrieves information in the database by using the Repository pattern. This pattern enabled the developers to isolate the database-specific aspects of the code inside a repository class, potentially allowing them to build different repository classes for different databases, and easily switching between them.

Note: The developers decided to use the Unity Application Block to enable the code to use different repositories without requiring that the web service is rebuilt and deployed. The Unity Application Block allows a developer to specify how an application should resolve references to objects at runtime, rather than at compile time. The section "Instantiating service and repository objects" contains a description of how the developers at Contoso designed the web service to support dynamic configuration by using the Unity Application Block.

The data that passes between the controllers and repository classes are domain entity objects. A domain entity object is a database-neutral type that models business-specific data. It provides a logical structure that hides the way that the database stores and retrieves the data. The details of how the database entities are converted into domain entity objects are described in the section "Decoupling entities from the data access technology."

The response messages that the controllers create and send back to the client applications are HTTP REST messages, in many cases wrapping the data that was requested by a client application, following the Data Transfer Object (DTO) pattern. The section "Transmitting data between a controller and a client" contains more information about the DTO objects created by the Contoso web service.

The following figure illustrates the high level flow of control through the Contoso web service. This figure depicts a client application sending a request that is handled by a controller in the web service. Controllers respond to requests, accessing data through services which use repositories. The repositories use database-specific repository objects to implement the data access logic used to retrieve and store data. The repositories also convert information between the database-specific format and neutral entity objects. The controller then uses the entity objects to construct DTOs that it passes back in the REST responses to the client apps.

The high-level flow of control through the Contoso web service

The high-level flow of control through the Contoso web service

Note

The Contoso web service implements only minimal security measures. Any application that knows the URL of the web service can connect to it and send requests. A real world implementation of this system would include more robust authentication, and prevent unauthorized use.

Routing incoming requests to a controller

All incoming REST requests are routed to a controller based on the URI that the client application specifies. The controllers are implemented by using the controller classes defined in the Controllers folder of the Contoso.Api project. The VideosController contains the following methods:

  • Get: Returns a paged collection of videos.
  • Get: Returns a video with a specified ID or a list of videos suitable for paging.
  • GetRecommendations: Returns a list of videos that users who viewed the specified video also viewed.
  • GetResolutions: Returns a list of resolutions from which a resolution can be selected to encode a video to.
  • GenerateAsset: Creates an empty asset in Media Services that corresponds to a video to be uploaded to blob storage.
  • Publish: Publishes a video uploaded to blob storage and submits it to the encoding pipeline for processing.
  • Update: Updates the video details for a video with a specified ID in the CMS.

Note

The Contoso web service also defines the HomeContoller class. This class is the controller that is associated with the root URI of the web service, and it generates the default Home view for the website. This view displays a welcome page describing the ASP.NET Web API.

The Contoso web service uses attribute-based routing, where the routes exposed by the web service are defined as attributes on controller classes and controller methods. Attribute-based routing is enabled by calling MapHttpAttributeRoutes during configuration.

The RoutePrefix attribute is used to annotate a controller class with a route prefix that applies to all methods within the controller. The route prefix for the VideosController class is api/videos. This route responds with a JSON object or a JSON array containing the data returned by the Get method of the specified controller, passing the ID as the parameter to this method. An example of a typical route is api/videos/71 which returns the video details for the video with an ID of 71.

Additional routes can be specified by using the Route attribute to annotate a controller method so that the method is exposed directly via a route. The following table summarizes the methods in the VideoController class are annotated with the Route attribute.

Method

Route

Example

GetRecommendations

{videoId:int}/recommendations

api/videos/77/recommendations

Resolutions

resolutions

api/videos/resolutions

GenerateAsset

generateasset

api/videos/generateasset

Publish

publish

api/videos/publish

These routes respond with a JSON object or a JSON array containing the data returned by the specified method of the controller, passing in parameters to the methods.

For more information about defining routes for Web API web services see "Attribute Routing in Web API 2."

Note

The web service is configured to accept and handle cross-origin resource sharing (CORS) requests. This feature enables the web client application to run in a separate web domain from the Contoso web service.

Transmitting data between a controller and a client

The controllers receive REST requests and send REST responses. The data received in these requests and sent in these responses can contain structured information, such as the details of a video. The Contoso web service defines a series of serializable data types that define the shape of this structured information. These types act as data transfer objects (DTO), and the information that they contain is transmitted as JSON objects and JSON arrays. These DTOs are implemented in the Models folder of the Contoso.Api project. The following table briefly describes these classes.

Model class

Description

VideoAssetDTO

Specifies the details of a video asset. The properties are the shared access signature locator, and an asset ID.

VideoDetailDTO

Holds the details of a video, including its ID, title, description, length, thumbnails, subtitles, captions, tags, and metadata.

VideoInfoDTO

Contains the ID, title, thumbnails, and length of a video.

VideoMetadataDTO

Holds the ID, name, and metadata for a video.

VideoPlayDTO

Specifies the URL and encoding type for video.

VideoPublishDTO

Inherits from the VideoSaveDTO, adding details required when publishing a video, such as the asset ID, clip times, whether thumbnails are included, and the resolution.

VideoSaveDTO

Contains the ID, title, description, and length of a video to be saved.

VideoThumbnailDTO

Specifies the ID and URL for a video's thumbnail.

The benefits of using DTOs to pass data to and receive data from a web service are that:

  • By transmitting more data in a single remote call, the app can reduce the number of remote calls. In most scenarios, a remote call carrying a large amount of data takes virtually the same time as a call that carries only a small amount of data.
  • Passing more data in a single remote call more effectively hides the internals of the web service behind a coarse-grained interface.
  • Defining a DTO can help in the discovery of meaningful business objects. When creating DTOs, you often notice groupings of elements that are presented to a user as a cohesive set of information. Often these groups serve as useful prototypes for objects that describe the business domain that the app deals with.
  • Encapsulating data into a serializable object can improve testability.

The Contoso web service uses AutoMapper to convert the domain entity objects that it receives from the various repository classes into DTOs. AutoMapper simplifies the process of translating objects from one format into another by defining a set of mapping rules that can be used throughout an application. In the Contoso web service, these rules are defined in the AutoMapperConfig.cs file in the App_Start folder in the Contoso.Api project.

As an example, the following code example shows the VideoInfo domain entity type that contains the data returned by the GetRecommendations method in the VideoRepository class.

public class VideoInfo
{
    private readonly IList<VideoThumbnail> thumbnails = 
        new List<VideoThumbnail>();

    public int Id { get; set; }

    public string Title { get; set; }

    public string Length { get; set; }
        
    public IReadOnlyCollection<VideoThumbnail> Thumbnails
    {
        get { return new ReadOnlyCollection<VideoThumbnail>(this.thumbnails); }
    }
        ...
}

The VideoInfoDTO class is shown in the following code example, and is returned by the GetRecommendations method of the VideosController class.

public class VideoInfoDTO
{
    public int Id { get; set; }
    public string Title { get; set; }
    public ICollection<VideoThumbnailDTO> Thumbnails { get; set; }
    public string Length { get; set; }
}

The AutoMapperConfig.cs file contains mappings that specify how to populate a VideoInfoDTO from a VideoInfo entity object, as shown in the following code example.

public static void SetAutoMapperConfiguration()
{
    Mapper.CreateMap<VideoInfo, VideoInfoDTO>();
    ...
}

This mapping specifies that the VideoInfoDTO properties should be read from the corresponding VideoInfo properties.

The GetRecommendations method in the VideosController class uses the static Map method of the Mapper class to translate a collection of VideoInfo domain entity objects returned by the VideoRepository class into a collection of VideoInfoDTO objects that it sends back in the REST response to the client application.

public async Task<HttpResponseMessage> GetRecommendations(int videoId)
{
    ...
    var videoInfos = await this.videoService.GetRecommendations(videoId);
    var result = new List<VideoInfoDTO>();

    Mapper.Map(videoInfos, result);

    return Request.CreateResponse(HttpStatusCode.OK, result);
    ...
}

For more information about AutoMapper, see "AutoMapper."

Using the Repository pattern to access data

The Repository pattern enables you to separate the code that accesses a data store from the business logic that uses the data in the store, minimizing any dependencies that the controller might have on the data store. The VideosController creates one or more service classes that use repository objects to connect to the database and retrieve, create, or update data. For example, the following code shows the Get method in the VideosController class that uses the GetVideo method of the VideoService class to fetch metadata for a video.

public async Task<HttpResponseMessage> Get(int id)
{
    ...
    var videoDetail = await this.videoService.GetVideo(id);
    ...
}

In turn, the GetVideo method of the VideoService class uses the GetVideo method of the VideoRepository class to fetch metadata for a video.

public async Task<VideoDetail> GetVideo(int id)
{
    var result = await this.videoRepository.GetVideo(id).ConfigureAwait(false);
    return result;
}

Each repository object is an instance of a database-specific repository class. The repository classes implement a set of database-agnostic interfaces. The methods defined by the repository interfaces return or manipulate collections and instances of domain entity objects. The following table summarizes the repository interfaces.

Repository interface

Methods

Description

IJobRepository

GetJob

SaveJob

This repository interface provides access to encoding job information.

The GetJob method returns an EncodingJob that matches the specified job ID.

The SaveJob method writes an EncodingJob domain entity object to the database.

IVideoRepository

GetEncodedVideos

GetVideo

GetRecommendations

SaveVideo

GetEncodedVideosCount

This repository interface manages videos in the database and provides access to recommendation information for a video.

The GetEncodedVideos method returns a collection of VideoInfo objects suitable for paging.

The GetVideo method returns a VideoDetail object that matches the specified ID.

The GetRecommendations method returns a collection of VideoInfo objects that match the specified ID.

The SaveVideo method saves the VideoDetail object passed into the method as a parameter to the database.

The GetEncodedVideosCount methods returns an integer that represents the number of completely encoded videos listed in the Content Management System database.

The repository interfaces are defined in the Contoso.Repositories project.

The repository classes and the data types that the repository classes use to retrieve and modify data in the SQL Server database, are defined in the Contoso.Repositories.Impl.Sql project. This project contains two repository classes named JobRepository and VideoRepository, that use the Entity Framework v6 to connect to the database. The classes provide the business methods that the service classes use to store and retrieve data. Internally, these repository classes retrieve and save data by using a context object (JobContext or VideoContext as appropriate). Each context object is responsible for connecting to the database and managing or retrieving data from the appropriate tables.

Retrieving and storing data in the database

The context classes are defined in the Contoso.Repositories.Impl.Sql project. They are all custom Entity Framework context objects derived from the DbContext class that exposes the data from the SQL Server database through a group of public IDbSet collection properties. The following code example shows how some of the properties for handling encoding jobs are defined in the JobContext class.

public class JobContext : DbContext
{
    ...
    public IDbSet<JobEntity> Jobs { get; set; }
    public IDbSet<TaskEntity> Tasks { get; set; }
    public IDbSet<AssetEntity> Assets { get; set; }
    public IDbSet<TaskAssetEntity> TaskAssets { get; set; }
    ...
}

The type parameters referenced by the IDbSet collection properties are entity classes that are specific to the Entity Framework repositories. They correspond to individual tables in the SQL Server database. These classes are defined in the DataEntities folder in the Contoso.Repositories.Impl.Sql project. The following table summarizes the entity classes.

Entity class

Description

AssetEntity

This class contains the asset ID of an asset being processed by a task in an encoding job.

BaseEntity

This is the base class from which all other entity classes derive, and defines the Id property.

JobEntity

This class contains information about an encoding job for a VideoEntity object, including the collection of TaskEntity objects that make up the encoding job.

MetadataEntity

This class contains the metadata name and value, and a collection of VideoEntity objects that the metadata value is applied to.

TaskAssetEntity

This class contains information about an AssetEntity object being processed by a TaskEntity object.

TaskEntity

This class contains information about a processing Task for a JobEntity object.

VideoEntity

This class contains the data that describes a video. Many of the properties correspond to fields in the Video table in the SQL Server database, and relationships between the Video table and other tables such as Metadata and VideoThumbnail are implemented as collections of the appropriate entity object.

VideoThumbnailEntity

This class contains the thumbnail information for a VideoEntity object.

VideoUrlEntity

This class describes the URL and encoding type for a VideoEntity object.

The Contoso developers followed a code first approach to implementing the entity model for the repositories and they defined each of the entity classes manually. The following code example shows the JobEntity class.

public class JobEntity : BaseEntity
{
    public string JobUuId { get; set; }
    public DateTime CreateDateTime { get; set; }
    public ICollection<TaskEntity> Tasks { get; set; }
    public int VideoId { get; set; }
    public virtual VideoEntity Video { get; set; }
}

The JobContext class uses the Entity Framework Fluent API to map the entity classes to tables in the SQL Server database and specify the relationships between tables. The following code example shows the OnModelCreating event handler method for the JobContext class that populates the Jobs, Tasks, Assets, and TaskAssetsIDbSet collections. The event handler runs when the JobContext object is created, and it passes a reference to the in-memory data model builder that is constructing the model for the Entity Framework as the parameter.

public class JobContext : DbContext
{
    ...
    public IDbSet<JobEntity> Jobs { get; set; }
    public IDbSet<TaskEntity> Tasks { get; set; }
    public IDbSet<AssetEntity> Assets { get; set; }
    public IDbSet<TaskAssetEntity> TaskAssets { get; set; }
    ...

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        ...
        modelBuilder.Entity<JobEntity>().Property(j => j.Id)
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);

        modelBuilder.Entity<JobEntity>()
            .ToTable("Job")
            .HasKey(j => j.Id);

        modelBuilder.Entity<JobEntity>()
            .HasMany(j => j.Tasks)
            .WithOptional()
            .HasForeignKey(t => t.JobId)
            .WillCascadeOnDelete(true);

        ...
    }
}

The code in this method describes how to associate each of the entity classes with the corresponding tables in the database. Specifically, the code stipulates that:

  • Instances of the JobEntity type should be mapped to rows in the Job table in the SQL Server database.
  • The Id field contains the primary key used to establish relationships with other objects.
  • The Job table in the database has a one-to-many relationship with the Task table, with the relationship also being a many to optional relationship.
  • The JobId property of the Task table is used as a foreign key.
  • The tasks for a job should be removed if the job is deleted.

For more information about constructing an entity model at runtime by using the Fluent API, see "Configuring/Mapping Properties and Types with the Fluent API" and "Configuring Relationships with the Fluent API."

Reading and writing the data for objects

In the context classes, each IDbSet property is an enumerable collection. This means that a repository object can retrieve data from a context object by performing LINQ queries. The following code example shows the GetJob method in the JobRepository class that uses a JobContext object to retrieve job data from the database through the JobsIDbSet collection.

public class JobRepository : IJobRepository
{
    ...
    public async Task<EncodingJob> GetJob(string jobId)
    {
        using (JobContext context = new JobContext(this.NameOrConnectionString))
        {
            var job = await context.Jobs.Include(j => j.Tasks.Select(t => 
                t.TaskAssets.Select(a => a.Asset))).SingleOrDefaultAsync(j => 
                j.JobUuId == jobId).ConfigureAwait(false);
            ...
        }
    }
}

The data for a JobEntity object also includes task information that's retrieved from a separate table in the SQL Server database. The Include method ensures that the tasks are populated when the data for the JobEntity object is retrieved from the database (the Entity Framework performs lazy evaluation by default).

All the changes made to the collections attached to a context object are packaged up and converted into the corresponding SQL commands by the SaveChangesAsync method inherited from the DbContext class. For example, the SaveJob method in the JobRepository class creates and populates:

  • TaskEntity objects that are added to the Tasks collection in the JobEntity object.
  • TaskAssetEntity objects that are added to the TaskAssets collection in the TaskEntity object.
  • AssetEntity objects that are added to the Asset collection in the TaskAssetEntity object.

The JobEntity object is then added to the Jobs collection in the DbContext object. The call to the SaveChangesAsync method ensures that the new job, tasks, task assets, and asset details are saved to the tables in the SQL Server database.

public async Task SaveJob(EncodingJob job)
{
    ...
    var jobEntity = new JobEntity()
    {
        CreateDateTime = DateTime.UtcNow,
        Id = job.Id,
        JobUuId = job.EncodingJobUuId,
        VideoId = job.VideoId,
        Tasks = new List<TaskEntity>()
    };

    foreach(var encodingTask in job.EncodingTasks)
    {
        var taskEntity = new TaskEntity() 
        { 
            Id = encodingTask.Id,
            TaskUuId = encodingTask.EncodingTaskUuId,
            TaskAssets = new List<TaskAssetEntity>(),
            JobId = job.Id
        };

        foreach(var encodingAsset in encodingTask.EncodingAssets)
        {
            taskEntity.TaskAssets.Add(new TaskAssetEntity() 
            { 
                Asset = new AssetEntity() 
                { 
                    AssetUuId = encodingAsset.EncodingAssetUuId,
                    Id = encodingAsset.Id
                },
                IsInputAsset = encodingAsset.IsInputAsset,
                Task = taskEntity,
                AssetId = encodingAsset.Id,
                TaskId = taskEntity.Id
            });
        }

        jobEntity.Tasks.Add(taskEntity);
    }

    using (JobContext context = new JobContext())
    {
        ...
        if(jobEntity.Id != 0)
        {
            context.Jobs.Attach(jobEntity);
            context.Entry(jobEntity).State = EntityState.Modified;
        }
        else
        {
            context.Jobs.Add(jobEntity);
        }
                   
        await context.SaveChangesAsync().ConfigureAwait(false);
    }
    ...
}

Decoupling entities from the data access technology

The data that passes between the controllers and service and repository classes are instances of domain entity objects. These are database-neutral types that help to remove any dependencies that the controller classes might otherwise have on the way that the data is physically stored, and can be found in the Contoso.Domain project.

The repository classes in the Contoso web service pass domain entity objects to the controller classes through the service classes. Therefore, an important task of the repository classes is to take the data that they retrieve from a database and return it as one or more of the domain entity objects.

For example, the VideoRepository class uses the VideoContext class to retrieve data from the database, which in turn uses a database-specific class. Therefore, the VideoRepository class must also convert the collection of data in the database-specific class to a collection of domain entity objects that can be passed to the relevant controller class. The following code example shows how this is accomplished in the GetRecommendations method.

public async Task<ICollection<VideoInfo>> GetRecommendations(int id)
{
    using (var context = new VideoContext(this.NameOrConnectionString))
    {
        // This code currently just pulls 5 randomly encoded videos just for the sake of example. Normally you would build you recommendation engine to your specific needs
        List<VideoEntity> videos = null;

        var random = new Random();
        var limit = context.Videos.Count(s => s.EncodingStatus == (int) EncodingState.Complete) - 5;
        var skip = random.Next(0, limit);
        videos = await context.Videos
            .Where(v => v.EncodingStatus == (int)EncodingState.Complete)
            .Include(v => v.Thumbnails)
            .OrderByDescending(v => v.LastUpdateDateTime)
            .Skip(skip).Take(5)
            .AsNoTracking()
            .ToListAsync()
            .ConfigureAwait(false);
                
        var result = new List<VideoInfo>();
        foreach (var video in videos)
        {
            var videoInfo = new VideoInfo()
            {
                Id = video.Id,
                Title = video.Title,
                Length = video.Length,
            };

            video.Thumbnails.ToList().ForEach(t => videoInfo.AddThumbnailUrl(new VideoThumbnail() { Id = t.Id, Url = t.ThumbnailUrl }));

                result.Add(videoInfo);
        }

        return result;
    }
}

The GetMetadata method retrieves data from the database by querying the Videos property in the VideoContext class, which is an enumerable collection of VideoEntity objects. The collection of VideoEntity objects is then converted into a collection of VideoInfo domain entity objects that can be passed to the VideosController class.

Instantiating service and repository objects

The service and repository classes implement a common set of interfaces that enabled the Contoso developers to decouple the business logic in the VideosController class from the database-specific code. However, the controller still has to instantiate the appropriate service classes, and each service class still has to instantiate the appropriate repository class. This process can introduce dependencies back into the code for the controller classes if it is not performed carefully. To eliminate these dependencies the Contoso developers chose to use the Unity Application Block to inject references to the service and repository classes.

The VideosController class references the services that it uses through the service interface. The following code example shows how the VideosController class references the VideoService and EncodingService class through the IVideoService and IEncodingService interfaces.

public VideosController(IVideoService videoService,
    IEncodingService encodingService)
{
    ...
    this.videoService = videoService;
    this.encodingService = encodingService;
}

The constructor is passed a reference to the IVideoService and IEncodingService objects that are used by the methods in the VideosController class. At runtime, the Contoso web service uses the Unity Application Block to resolve the IVideoService and IEncodingService references that were passed to the VideosController constructor, and creates new VideoService and EncodingService objects. The UnityConfig class in the App_Start folder of the Contoso.Api project is responsible for registering the type mappings with the Unity container. The following code example shows the RegisterTypes method from the UnityConfig class.

public static void RegisterTypes(IUnityContainer container)
{
    ...
    container.RegisterType<IVideoService, VideoService>(
        new PerResolveLifetimeManager());
    container.RegisterType<IEncodingService, EncodingService>(
        new PerResolveLifetimeManager());
}

This registration enables the Unity Application Block to resolve references to the IVideoService and IEncodingService interfaces as instances of the VideoService and EncodingService class in the Contoso.Domain.Services.Impl assembly.

The same mechanism is used in service classes to instantiate repository classes. For example, the VideoService constructor is passed an IVideoRepository object that is used by methods in the VideoService class. At runtime, the Contoso web service uses the Unity Application Block to resolve the IVideoRepository reference passed to the VideoService constructor and creates a new VideoRepository object.

For more information about using the Unity Application Block to resolve dependencies, see "Unity Container."

More information

Next Topic | Previous Topic | Home | Community