How to write elegant, structured code using services, authorizers, validators and repositories

This article describes how to build a RESTful API with ASP.NET Core using components called services, authorizers, validators and repositories.

Posted
8 December 2016

In this article, I show how to write elegant, structured code using components called services, authorizers, validators and repositories. To illustrate each of these components, I describe a RESTful API built with ASP.NET Core.

The diagram below shows how each of the components interact with one another. On the left hand side of the diagram are ASP.NET Core controllers that handle browser requests. Controllers call services, which may in turn call validators and authorizers or execute business logic. Validators, authorizers and business logic may then call repositories to update or query underlying storage. Models are simple types that are passed between components, from controllers all the way down to repositories and back again. The design patterns described in this article can be reused again and again to build applications that are robust and highly testable.

Diagram showing controllers, services, authorizers, validators and repositories

The RESTful API described in this article can be used to administer (create, read, update, delete and list) announcements. Announcments consist of a title and description and are displayed for a given period of time. Announcements have a severity (low, medium or high) and a status (pending approval or approved).

A full list of announcement fields, along with rules governing permitted values for these fields, is shown below. The rules stated are enforced by a validator.

  • Title is a mandatory text field and can be no more than 256 characters in length.
  • Description is an optional text field.
  • Severity is a mandatory field that is set to low, medium or high.
  • Status is a mandatory field that is either pending approval or approved. Newly created announcements have initial status set to pending approval.
  • Display from is an optional date time field that determines when an approved announcement is shown. If left blank, announcement is considered visible from the beginning of time.
  • Display to is an optional date time field that determines when an approved announcement is hidden. If left blank, announcement is considered visible until the end of time. When an announcement is created or updated, the display to field must be left blank or occur in the future.
  • If both display from and display to date time fields are specified, then the display from date time field must occur before the display to date time field.
  • Created is a mandatory date time field that is automatically populated with the current date and time when an announcement is created. It cannot be changed.
  • Updated is initially blank, but subsequently populated with the current date and time whenever an announcement is updated.

The business rules listed below determine who can administer announcements and under what circumstances. These rules are enforced by an authorizer.

  • Administrators and editors can create announcements.
  • Administrators can update all announcements.
  • Editors can only update announcements with status pending approval and they cannot change the status of an announcement.
  • Administrators can delete all announcements.
  • Editors can only delete announcements with status pending approval.
  • Administrators and editors can view all announcements at any time.
  • Users who are neither administrators or editors can only view "active" announcements. Announcments are considered "active" if they have status approved and the current date and time falls within the time period defined by the display from and display to date time fields.

Let's start by looking at the announcement model.

Models

Models are simple types that contain property getters and setters. Data annotations may be applied to assist in model validation and to identify fields. However, the main thing to note is that models should be as straightforward as possible. The Announcement model is shown below.

using System;
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;

public class Announcement
{
    [JsonProperty(PropertyName = "announcementId")]
    public string AnnouncementId { get; set; }

    [JsonProperty(PropertyName = "title")]
    [Display(ResourceType = typeof(AnnouncementResource), Name = "TitleLabel")]
    [Required(ErrorMessageResourceType = typeof(AnnouncementResource), ErrorMessageResourceName = "TitleRequiredMessage")]
    [StringLength(AnnouncementLengths.TitleMaxLength, ErrorMessageResourceType = typeof(AnnouncementResource), ErrorMessageResourceName = "TitleMaxLengthMessage")]
    public string Title { get; set; }

    [JsonProperty(PropertyName = "description")]
    [Display(ResourceType = typeof(AnnouncementResource), Name = "DescriptionLabel")]
    public string Description { get; set; }

    [JsonProperty(PropertyName = "displayFrom")]
    [Display(ResourceType = typeof(AnnouncementResource), Name = "DisplayFromLabel")]
    public DateTime? DisplayFrom { get; set; }

    [JsonProperty(PropertyName = "displayTo")]
    [Display(ResourceType = typeof(AnnouncementResource), Name = "DisplayToLabel")]
    public DateTime? DisplayTo { get; set; }

    [JsonProperty(PropertyName = "status")]
    [Display(ResourceType = typeof(AnnouncementResource), Name = "StatusLabel")]
    public AnnouncementStatus Status { get; set; }

    [JsonProperty(PropertyName = "severity")]
    [Display(ResourceType = typeof(AnnouncementResource), Name = "SeverityLabel")]
    public AnnouncementSeverity Severity { get; set; }

    [JsonProperty(PropertyName = "created")]
    [Display(ResourceType = typeof(AnnouncementResource), Name = "CreatedLabel")]
    public DateTime Created { get; set; }

    [JsonProperty(PropertyName = "updated")]
    [Display(ResourceType = typeof(AnnouncementResource), Name = "UpdatedLabel")]
    public DateTime? Updated { get; set; }
}

The Announcement type

The Required data annotation attribute indicates that Title is a mandatory field, while the StringLength attribute indicates that Title should be no longer than AnnouncementLengths.TitleMaxLength (256) characters in length. The ErrorMessageResourceType and ErrorMessageResourceName properties are used to locate error messages in resource files that are shown when validations fail. Error messages can be inlined directly into data annotations, however the use of resource files (as shown above) is highly recommended. This is especially true if you are working on an application that suppports globalization and localization.

The Display attribute is used to provide names for fields that might, for example, appear on labels in a user interface. Finally, the JsonProperty attribute comes from Newtonsoft's Json.NET framework and controls how .NET properties are serialised to JSON. In the above code, JsonProperty is used to ensure .NET properties are serialised to JSON using lower camel case. Without the JsonProperty attribute, ASP.NET Core serialises properties to JSON as they are defined in the C# code (i.e. typically using upper camel case).

By now, you should have a pretty good idea what models are. Next let's take a look at services.

Services

Services are where the bulk of your application's business logic is found. The service shown below is used to administer announcements. There are methods for creating, reading, updating, deleting and enumerating announcements.

using System;
using System.Collections.Generic;

public interface IAnnouncementService
{
    string Create(Announcement announcement);
    Announcement Read(string announcementId);
    void Update(Announcement announcement);
    void Delete(string announcementId);
    IEnumerable<Announcement> ListAll();
    IEnumerable<Announcement> ListActive();
}

public class AnnouncementService : IAnnouncementService
{
    private IAnnouncementAuthorizer _authorizer;
    private IAnnouncementRepository _repository;
    private IAnnouncementValidator _validator;

    public AnnouncementService(IAnnouncementAuthorizer authorizer, IAnnouncementRepository repository, IAnnouncementValidator validator)
    {
        _authorizer = authorizer;
        _repository = repository;
        _validator = validator;
    }

    private void PrepareFields(Announcement announcement)
    {
        announcement.Title = announcement.Title.Trim();
        announcement.Description = (announcement.Description ?? string.Empty).Trim();
    }

    public string Create(Announcement announcement)
    {
        // Can user perform this action?
        _authorizer.AuthorizeCreate(announcement);

        // Set server side only properties
        announcement.Created = DateTime.UtcNow;
        announcement.Status = AnnouncementStatus.PendingApproval;

        // Is supplied data correct?
        _validator.ValidateCreate(announcement);

        // Ensure fields in correct format
        PrepareFields(announcement);

        // Create announcement and return newly allocated announcement identifier
        return _repository.Create(announcement);
    }

    public Announcement Read(string announcementId)
    {
        // Can user perform this action?
        _authorizer.AuthorizeRead(announcementId);

        // Return the announcement
        return _repository.Read(announcementId);
    }

    public void Update(Announcement announcement)
    {
        // Can user perform this action?
        _authorizer.AuthorizeUpdate(announcement);

        // Set server side only properties
        announcement.Updated = DateTime.UtcNow;

        // Is supplied data correct?
        _validator.ValidateUpdate(announcement);

        // Ensure fields in correct format
        PrepareFields(announcement);

        // Do the update
        _repository.Update(announcement);
    }

    public void Delete(string announcementId)
    {
        // Can user perform this action?
        _authorizer.AuthorizeDelete(announcementId);

        // Do the delete
        _repository.Delete(announcementId);
    }

    public IEnumerable<Announcement> ListAll()
    {
        // Can user perform this action?
        _authorizer.AuthorizeListAll();

        // Return all announcements
        return _repository.ListAll();
    }

    public IEnumerable<Announcement> ListActive()
    {
        // Everyone can see active announcements
        return _repository.ListActive(DateTime.UtcNow);
    }
}

The announcement service

The service shown above has a single responsibility, which is to administer announcements. The single responsibility principle is one of the first five principles of object oriented programming. In fact, if you were following SOLID prinicples to a T, you might split the above announcements service into 5 smaller services, where each service implements one of the basic functions of persistent storage (create, read, update, delete or list). However, that approach results in a proliferation of classes and interfaces and I prefer to write broader services that implement a number of related features. Either way, single responsibility helps to ensure a robust design and legible code.

AnnouncementService is dependent on three other components: IAnnouncementAuthorizer, IAnnouncementValidator and IAnnouncementRepository. These components are responsible for i) checking that a user is allowed to perform an action, ii) validating supplied data and iii) updating underlying storage.

Dependent components are passed to the announcement service via the AnnouncementService constructor. In this way, the service does not have to create dependent components internally. The constructor parameters and member variables are interface types, which means the AnnouncementService class is not tied to specific implementations of dependent components. This ensures loose coupling between the service and dependent components. This design pattern is known as dependency injection and allows us to create types that are independent, maintainable, reusable and easier to unit test. All services should be designed this way, with dependency injection in mind.

You have probably noticed that the AnnouncementService class implements the interface IAnnouncementService. This allows the announcement service iteself to become a dependent component. In the same way that an authorizer, validator and repository can be injected into the AnnouncementService class, so the announcement service can be injected into another type such as an ASP.NET Core controller.

As well as calling authorizers and validators, the AnnouncementService class contains a PrepareFields method for stripping leading and trailing spaces from title and description fields. The announcement service also ensures that properties such as created, updated and status are correctly set. Next we look at components that AnnouncementService depends upon.

Authorizers

Authorizers have a single responsibility to protect services from unwarranted access. The announcement authorizer, shown below, implements methods that protect the create, read, update, delete and list all announcement service actions. If a user is not authorized to perform a particular action, an AuthorizerException is thrown that contains details of the authorization failure. Authorizer exceptions contain a collection of AuthorizerError objects that are used to record the model property (or field) that an authorization error is associated with along with an appropriate error message. Let's take a look at some code.

public interface IAnnouncementAuthorizer
{
    void AuthorizeCreate(Announcement announcement);
    void AuthorizeRead(string announcementId);
    void AuthorizeUpdate(Announcement announcement);
    void AuthorizeDelete(string announcementId);
    void AuthorizeListAll();
}

public class AnnouncementAuthorizer : IAnnouncementAuthorizer
{
    private IAnnouncementRepository _repository;
    private IAuthorizationService _authorizationService;

    public AnnouncementAuthorizer(IAnnouncementRepository repository, IAuthorizationService authorizationService)
    {
        _repository = repository;
        _authorizationService = authorizationService;
    }

    public void AuthorizeCreate(Announcement announcement)
    {
        // User must be an editor or administrator to create an announcement
        if (!_authorizationService.UserIsInRole(Roles.Editor) && !_authorizationService.UserIsInRole(Roles.Administrator))
            throw new AuthorizerException(new AuthorizerError(null, AnnouncementResource.CreateInvalidMessage));
    }

    public void AuthorizeRead(string announcementId)
    {
        // If user is editor or administrator, no futher checks required
        if (_authorizationService.UserIsInRole(Roles.Editor) || _authorizationService.UserIsInRole(Roles.Administrator))
            return;

        // Get announcement
        Announcement announcement = _repository.Read(announcementId);

        // Only editors and administrators can view announcements that are pending approval
        if (announcement.Status == AnnouncementStatus.PendingApproval)
            throw new AuthorizerException(new AuthorizerError(null, AnnouncementResource.ReadInvalidMessage));

        // Only editors and administrators can view announcements that are not active
        DateTime now = DateTime.UtcNow;
        DateTime from = announcement.DisplayFrom ?? DateTime.MinValue;
        DateTime to = announcement.DisplayFrom ?? DateTime.MaxValue;
        if (now < from || now > to)
            throw new AuthorizerException(new AuthorizerError(null, AnnouncementResource.ReadInvalidMessage));
    }

    public void AuthorizeUpdate(Announcement announcement)
    {
        // User must be an editor or administrator to update an announcement
        if (!_authorizationService.UserIsInRole(Roles.Editor) && !_authorizationService.UserIsInRole(Roles.Administrator))
            throw new AuthorizerException(new AuthorizerError(null, AnnouncementResource.UpdateInvalidMessage));

        // If user is administrator, no futher checks required
        if (_authorizationService.UserIsInRole(Roles.Administrator))
            return;

        // Get announcement as it currently stands so we can see if status is changing
        Announcement currentAnnouncement = _repository.Read(announcement.AnnouncementId);

        // If status approved, then can only be updated by administrator
        if (currentAnnouncement.Status == AnnouncementStatus.Approved)
            throw new AuthorizerException(new AuthorizerError(AnnouncementPropertyNames.Status, AnnouncementResource.ApprovedUpdateInvalidMessage));

        // A change of status can only be executed by an administrator
        if (currentAnnouncement.Status != announcement.Status)
            throw new AuthorizerException(new AuthorizerError(AnnouncementPropertyNames.Status, AnnouncementResource.StatusUpdateInvalidMessage));
    }

    public void AuthorizeDelete(string announcementId)
    {
        // User must be an editor or administrator to delete an announcement
        if (!_authorizationService.UserIsInRole(Roles.Editor) && !_authorizationService.UserIsInRole(Roles.Administrator))
            throw new AuthorizerException(new AuthorizerError(null, AnnouncementResource.DeleteInvalidMessage));

        // If user is administrator, no futher checks required
        if (_authorizationService.UserIsInRole(Roles.Administrator))
            return;

        // Get announcement as it currently stands so we can check status
        Announcement currentAnnouncement = _repository.Read(announcementId);

        // If status is approved, announcement can only be deleted by an administrator
        if (currentAnnouncement.Status == AnnouncementStatus.Approved)
            throw new AuthorizerException(new AuthorizerError(AnnouncementPropertyNames.Status, AnnouncementResource.StatusDeleteInvalidMessage));
    }

    public void AuthorizeListAll()
    {
        // User must be an editor or administrator to list all announcements
        if (!_authorizationService.UserIsInRole(Roles.Editor) && !_authorizationService.UserIsInRole(Roles.Administrator))
            throw new AuthorizerException(new AuthorizerError(null, AnnouncementResource.ListAllInvalidMessage));
    }
}

The announcement authorizer

AnnouncementAuthorizer is dependent on two components: IAnnouncementRepository and IAuthorizationService. These components are responsible for i) retrieving announcement details from underlying storage and ii) checking whether the logged on user is a member of a given role. The Roles type is a static class that contains two constant strings identifying roles described earlier in this article: "Administrator" and "Editor". Loose coupling between the authorizer and dependent components is achieved using dependency injection via constructor.

It's worth pointing out that depending on how your code is structured, you may not need to implement all of the basic role checks shown above. For example, in order to update an announcement the logged on user must be either an administrator or an editor. The first line of AuthorizeUpdate performs this role check. However, if your service is called by a controller, the ASP.NET Core framework can perform upfront role checks via Authorize attributes. I'll cover ASP.NET Core authentication and authorization in a future article.

The AuthorizerError and AuthorizerException types are shown below.

using System;
using System.Collections.Generic;
using Newtonsoft.Json;

public class AuthorizerError
{
    public AuthorizerError(string key, string message)
    {
        Key = key;
        Message = message;
    }

    [JsonProperty(PropertyName = "key")]
    public string Key { get; set; }

    [JsonProperty(PropertyName = "message")]
    public string Message { get; set; }
}

public class AuthorizerException : Exception
{
    private IEnumerable<AuthorizerError> _errors;

    public AuthorizerException() { }
    public AuthorizerException(string message) : base(message) { }
    public AuthorizerException(string message, Exception inner) : base(message, inner) { }

    public AuthorizerException(IEnumerable<AuthorizerError> errors) { _errors = errors; }
    public AuthorizerException(IEnumerable<AuthorizerError> errors, string message) : base(message) { _errors = errors; }
    public AuthorizerException(IEnumerable<AuthorizerError> errors, string message, Exception inner) : base(message, inner) { _errors = errors; }
    public AuthorizerException(AuthorizerError error) { _errors = new List<AuthorizerError> { error }; }
    public AuthorizerException(AuthorizerError error, string message) : base(message) { _errors = new List<AuthorizerError> { error }; }
    public AuthorizerException(AuthorizerError error, string message, Exception inner) : base(message, inner) { _errors = new List<AuthorizerError> { error }; }

    public IEnumerable<AuthorizerError> Errors { get { return _errors; } }
}

The authorizer error and authorizer exception types

Validators

Validators have a single responsibility to validate models passed to services. This typically involves using data annotations to validate models, as well as executing business logic where validation rules cannot be expressed soley by data annotations. The announcement validator, shown below, validates Announcement models that are passed to Create and Update service methods.

using System;

public interface IAnnouncementValidator
{
    void ValidateCreate(Announcement announcement);
    void ValidateUpdate(Announcement announcement);
}

public class AnnouncementValidator : IAnnouncementValidator
{
    private IModelValidator _modelValidator;

    public AnnouncementValidator(IModelValidator modelValidator)
    {
        _modelValidator = modelValidator;
    }

    private void ValidateModelAndDates(Announcement announcement)
    {
        // Do stock model validation (title is required)
        _modelValidator.Validate(announcement);

        // If specified, "display to" must be in the future
        if (announcement.DisplayTo.HasValue && announcement.DisplayTo.Value < DateTime.UtcNow)
            throw new ValidatorException(new ValidatorError(AnnouncementPropertyNames.DisplayTo, AnnouncementResource.DisplayToInvalidMessage));

        // If "display from" and "display to" specified, from date must be before to date
        if (announcement.DisplayFrom.HasValue && announcement.DisplayTo.HasValue && announcement.DisplayTo.Value <= announcement.DisplayFrom.Value)
            throw new ValidatorException(new ValidatorError(null, AnnouncementResource.DisplayRangeInvalidMessage));
    }

    public void ValidateCreate(Announcement announcement)
    {
        ValidateModelAndDates(announcement);
    }

    public void ValidateUpdate(Announcement announcement)
    {
        ValidateModelAndDates(announcement);
    }
}

The annnouncement validator

When an invalid model is encountered by the announcement validator, a ValidatorException is thrown that contains details of the validation failure. Validator exceptions contain a collection of ValidatorError objects that are used to record the model property that a validation error is associated with along with an appropriate error message. The ValidatorError and ValidatorException types are shown below.

using System;
using System.Collections.Generic;
using Newtonsoft.Json;

public class ValidatorError
{
    public ValidatorError(string key, string message)
    {
        Key = key;
        Message = message;
    }

    [JsonProperty(PropertyName = "key")]
    public string Key { get; set; }

    [JsonProperty(PropertyName = "message")]
    public string Message { get; set; }
}

public class ValidatorException : Exception
{
    private IEnumerable<ValidatorError> _errors;

    public ValidatorException() { }
    public ValidatorException(string message) : base(message) { }
    public ValidatorException(string message, Exception inner) : base(message, inner) { }

    public ValidatorException(IEnumerable<ValidatorError> errors) { _errors = errors; }
    public ValidatorException(IEnumerable<ValidatorError> errors, string message) : base(message) { _errors = errors; }
    public ValidatorException(IEnumerable<ValidatorError> errors, string message, Exception inner) : base(message, inner) { _errors = errors; }
    public ValidatorException(ValidatorError error) { _errors = new List<ValidatorError> { error }; }
    public ValidatorException(ValidatorError error, string message) : base(message) { _errors = new List<ValidatorError> { error }; }
    public ValidatorException(ValidatorError error, string message, Exception inner) : base(message, inner) { _errors = new List<ValidatorError> { error }; }

    public IEnumerable<ValidatorError> Errors { get { return _errors; } }
}

The validator error and validator exception types

AnnouncementValidator is dependent on IModelValidator. Loose coupling between the announcement and model validators is achieved using dependency injection via constructor. The model validator has a single Validate method that throws a ValidatorException if any of the data annotations associated with a model cause a validation failure. Let's take a look at the model validator.

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

public interface IModelValidator
{
    void Validate(object model);
}

public class ModelValidator : IModelValidator
{
    public void Validate(object model)
    {
        List<ValidatorError> errors = new List<ValidatorError>();
        ValidationContext context = new ValidationContext(model, null, null);
        List<ValidationResult> results = new List<ValidationResult>();
        if (!Validator.TryValidateObject(model, context, results, true))
        {
            foreach (ValidationResult result in results)
            {
                foreach (string memberName in result.MemberNames)
                {
                    errors.Add(new ValidatorError(memberName, result.ErrorMessage));
                }
            }
        }
        if (errors.Count > 0)
            throw new ValidatorException(errors);
    }
}

The model validator

Only the Title property of the Announcement class has data annotations related to validation. The Required attribute ensures that title is specified, while the StringLength attribute ensures that title is no longer than 256 characters in length. If title is not specified or is greater than 256 characters in length, then calls to either of the announcement validator's ValidateCreate or ValidateUpdate methods will result in the model validator throwing a validator exception.

Finally, the announcement validator contains business logic to validate an announcement's DisplayFrom and DisplayTo fields. If DisplayTo is specified, then it must be set to a date time in the future (otherwise the announcement is never displayed). Furthermore, if both DisplayFrom and DisplayTo fields are specified, then DisplayFrom must occur before DisplayTo. It's worth noting that where a validator error cannot be associated directly with a specific model property (for example, in the case just described where two properties represent an invalid time period), then validator error key can be set null.

Now that we've covered validators, let's look at repositories.

Repositories

Repositories have a single responsibility to query and update underlying storage. The repository design pattern isolates business logic from data access code that interacts with underlying storage. The interface IAnnouncementRepository allows us to loosely couple the announcement repository with other components such as services, validators and authorizers. This ensures that components are not tied to specific implementations of repositories, which in turn means that components are not tied to any specific underlying storage. If we want to use SQL Server as our database, we can inject repositories that contain SQL Server specific data access code. If we want to use DocumentDB as our database, we can inject repositories that interact with Azure's DocumentDB NoSQL document database service.

Let's take a look at the IAnnouncementRepository interface.

using System;
using System.Collections.Generic;

public interface IAnnouncementRepository
{
    string Create(Announcement announcement);
    Announcement Read(string announcementId);
    void Update(Announcement announcement);
    void Delete(string announcementId);
    IEnumerable<Announcement> ListAll();
    IEnumerable<Announcement> ListActive(DateTime now);
}

The IAnnouncementRepository interface

Types that implement IAnnouncementRepository must contain methods for creating, reading, updating, deleting, enumerating active and enumerating all announcements. Consider an announcement repository that uses SQL Server to persist data and ADO.NET to execute SQL queries. For brevity, only create and read actions are shown here. Let's start by looking at the definition of a SQL Server table that can be used to store announcements.

CREATE TABLE [amt].[Announcement](
    [AnnouncementId] [bigint] IDENTITY(1,1) NOT NULL,
    [Title] [nvarchar](256) NOT NULL,
    [Description] [nvarchar](max) NOT NULL,
    [DisplayFrom] [datetime] NOT NULL,
    [DisplayTo] [datetime] NOT NULL,
    [Severity] [int] NOT NULL,
    [Status] [int] NOT NULL,
    [Created] [datetime] NOT NULL,
    [Updated] [datetime] NOT NULL,
    CONSTRAINT [PK_Announcement] PRIMARY KEY CLUSTERED ([AnnouncementId] ASC)
)

The Announcement table

The following two SQL queries can be used to create and read announcements.

SET NOCOUNT ON

INSERT INTO
    amt.Announcement (Title, [Description], DisplayFrom, DisplayTo, Severity, [Status], Created, Updated)
VALUES
    (@Title, @Description, @DisplayFrom, @DisplayTo, @Severity, @Status, @Created, @Updated)

SET @AnnouncementId = SCOPE_IDENTITY()

The create announcement SQL query

SET NOCOUNT ON

SELECT
    amt.Announcement.AnnouncementId,
    amt.Announcement.Title,
    amt.Announcement.[Description],
    amt.Announcement.DisplayFrom,
    amt.Announcement.DisplayTo,
    amt.Announcement.Severity,
    amt.Announcement.[Status],
    amt.Announcement.Created,
    amt.Announcement.Updated
FROM
    amt.Announcement
WHERE
    amt.Announcement.AnnouncementId = @AnnouncementId

The read announcement SQL query

Finally, let's take a look at our implementation of IAnnouncementRepository. Again, for brevity only create and read actions are shown below.

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.Extensions.Options;

public class SqlAnnouncementRepository : IAnnouncementRepository
{
    private IOptions<AnnouncementOptions> _options;
    private IResourceManager _resourceManager;

    private const string AssemblyName = "CodeCallback.Architecture.Announcements";

    public SqlAnnouncementRepository(IOptions<AnnouncementOptions> options, IResourceManager resourceManager)
    {
        _options = options;
        _resourceManager = resourceManager;
    }

    public string Create(Announcement announcement)
    {
        using (SqlConnection connection = new SqlConnection(_options.Value.ConnectionString))
        {
            connection.Open();
            string sql = _resourceManager.GetText(AssemblyName, "Sql.CreateAnnouncement.sql");
            using (SqlCommand command = new SqlCommand(sql, connection))
            {
                command.CommandType = CommandType.Text;
                command.Parameters.Add("@Title", SqlDbType.NVarChar, 256).Value = announcement.Title;
                command.Parameters.Add("@Description", SqlDbType.NVarChar, -1).Value = announcement.Description;
                command.Parameters.Add("@DisplayFrom", SqlDbType.DateTime).Value = announcement.DisplayFrom ?? SqlDateTime.MinValue.Value;
                command.Parameters.Add("@DisplayTo", SqlDbType.DateTime).Value = announcement.DisplayTo ?? SqlDateTime.MaxValue.Value;
                command.Parameters.Add("@Severity", SqlDbType.Int).Value = (int)announcement.Severity;
                command.Parameters.Add("@Status", SqlDbType.Int).Value = (int)announcement.Status;
                command.Parameters.Add("@Updated", SqlDbType.DateTime).Value = announcement.Updated ?? SqlDateTime.MinValue.Value;
                command.Parameters.Add("@Created", SqlDbType.DateTime).Value = announcement.Created;
                SqlParameter outputParameter = command.Parameters.Add("@AnnouncementId", SqlDbType.BigInt);
                outputParameter.Direction = ParameterDirection.Output;
                command.ExecuteNonQuery();
                return ((long)outputParameter.Value).ToString();
            }
        }
    }

    public Announcement Read(string announcementId)
    {
        Announcement announcement = null;
        using (SqlConnection connection = new SqlConnection(_options.Value.ConnectionString))
        {
            connection.Open();
            string sql = _resourceManager.GetText(AssemblyName, "Sql.ReadAnnouncement.sql");
            using (SqlCommand command = new SqlCommand(sql, connection))
            {
                command.CommandType = CommandType.Text;
                command.Parameters.Add("@AnnouncementId", SqlDbType.BigInt).Value = Convert.ToInt64(announcementId);
                using (SqlDataReader dr = command.ExecuteReader())
                {
                    if (dr.Read())
                    {
                        announcement = new Announcement {
                            Created = (DateTime)dr["Created"],
                            Description = (string)dr["Description"],
                            DisplayFrom = (DateTime)dr["DisplayFrom"] == SqlDateTime.MinValue.Value ? null : (DateTime?)dr["DisplayFrom"],
                            DisplayTo = (DateTime)dr["DisplayTo"] == SqlDateTime.MaxValue.Value ? null : (DateTime?)dr["DisplayTo"],
                            AnnouncementId = Convert.ToString((long)dr["AnnouncementId"]),
                            Severity = (AnnouncementSeverity)(int)dr["Severity"],
                            Status = (AnnouncementStatus)(int)dr["Status"],
                            Title = (string)dr["Title"],
                            Updated = (DateTime)dr["Updated"] == SqlDateTime.MinValue.Value ? null : (DateTime?)dr["Updated"]
                        };
                    }
                }
            }
        }
        return announcement;
    }

    ...
}

Create and read methods of the SqlAnnouncementRepository class

SqlAnnouncementRepository is dependent on two components: IOptions<AnnouncementOptions> and IResourceManager. These components are responsible for i) obtaining a database connection string from an appsettings.json file and ii) retrieving SQL queries from embedded resource files.

Repositories are complex and somewhat outside the scope of this article. However, the main thing to take away from this section is that repositories allow business logic to be isolated from data access code.

Controllers

In the final section of this article, I show how the announcement service described above can be used to build a RESTful announcements API running on ASP.NET Core. At the heart of an ASP.NET Core application are controllers that are repsonsible for performing actions in response to HTTP requests and returning HTTP responses. The code within a controller should be kept as straightforward as possible, with complex tasks delegated out to other components. The announcements controller is shown below.

using Microsoft.AspNetCore.Mvc;

[Route("[controller]")]
public class AnnouncementsController : Controller
{
    private IAnnouncementService _service;

    public AnnouncementsController(IAnnouncementService service)
    {
        _service = service;
    }

    [HttpGet("{id}")]
    public IActionResult Get(string id)
    {
        try
        {
            Announcement announcement = _service.Read(id);
            if (announcement == null)
                return NotFound();
            else
                return Ok(announcement);
        }
        catch (AuthorizerException ex)
        {
            return BadRequest(ex.Errors);
        }
        catch (ValidatorException ex)
        {
            return BadRequest(ex.Errors);
        }
    }

    [HttpPost]
    public IActionResult Post([FromBody]Announcement announcement)
    {
        try
        {
            return Ok(_service.Create(announcement));
        }
        catch (AuthorizerException ex)
        {
            return BadRequest(ex.Errors);
        }
        catch (ValidatorException ex)
        {
            return BadRequest(ex.Errors);
        }
    }

    [HttpPut("{id}")]
    public IActionResult Put(string id, [FromBody]Announcement announcement)
    {
        try
        {
            announcement.AnnouncementId = id;
            _service.Update(announcement);
            return Ok();
        }
        catch (AuthorizerException ex)
        {
            return BadRequest(ex.Errors);
        }
        catch (ValidatorException ex)
        {
            return BadRequest(ex.Errors);
        }
    }

    [HttpDelete("{id}")]
    public IActionResult Delete(string id)
    {
        try
        {
            _service.Delete(id);
            return Ok();
        }
        catch (AuthorizerException ex)
        {
            return BadRequest(ex.Errors);
        }
        catch (ValidatorException ex)
        {
            return BadRequest(ex.Errors);
        }
    }

    [HttpGet]
    public IActionResult Get(bool showAll)
    {
        try
        {
            if (showAll)
                return Ok(_service.ListAll());
            else
                return Ok(_service.ListActive());
        }
        catch (AuthorizerException ex)
        {
            return BadRequest(ex.Errors);
        }
        catch (ValidatorException ex)
        {
            return BadRequest(ex.Errors);
        }
    }
}

The announcements controller

The announcements controller is dependent on IAnnouncementService. Loose coupling between the controller and announcement service is achieved using dependency injection via constructor. The announcement controller has two Get methods that are called in response to HTTP GET requests. The first method returns a specific announcement, while the second method returns a list of announcements. The list of announcements returned depends on the showAll flag, which can be specified on the query string of an appropriate HTTP GET request. Post, Put and Delete methods are called in response to HTTP POST, HTTP PUT and HTTP DELETE requests and are responsible for creating, updating and deleting annnouncements.

All of the controller methods shown above follow a similar pattern. Business logic is executed within a try catch block. If an authorizer or validator exception is thrown, then the collection of errors associated with the exception is serialized into JSON and returned to the caller. The command line tool, curl can be used to issue HTTP requests. Example curl commands for administering announcements are shown below.

curl -i -X POST http://localhost:55322/announcements -H "Content-Type: application/json" -d "{}"
Response: HTTP/1.1 400 Bad Request
Content returned: [{"key":"Title","message":"Title is required"}]

curl -i -X POST http://localhost:55322/announcements -H "Content-Type: application/json" -d "{ \"title\": \"Announcement title\" }"
Response: HTTP/1.1 200 OK
Content returned: Newly allocated announcement identifier

curl -i -X GET http://localhost:55322/announcements/1
Response: HTTP/1.1 200 OK
Example content returned: {"announcementId":"1","title":"Announcement title updated","description":"","displayFrom":null,"displayTo":null,"status":0,"severity":0,"created":"2016-12-07T12:39:07.637","updated":"2016-12-07T12:42:17.12"}

curl -i -X PUT http://localhost:55322/announcements/1 -H "Content-Type: application/json" -d "{ \"title\": \"Announcement title updated\", ... }"
Response: HTTP/1.1 200 OK

curl -i -X DELETE http://localhost:55322/announcements/2
Response: HTTP/1.1 200 OK

curl -i -X GET http://localhost:55322/announcements
curl -i -X GET http://localhost:55322/announcements?showall=true
curl -i -X GET http://localhost:55322/announcements?showall=false
Response: HTTP/1.1 200 OK
Example content returned: [{"announcementId":"1","title":"Announcement title updated", ... }, {"announcementId":"2","title":"Another announcement", ... }]

Curl commands for administering announcements

Summary

Hopefully I've convinced you that the components and design patterns described here can help you to write elegant, structured code. Please feel free to post any comments or questions in the forum below. I look forward to reading any feedback!

Article written by Mike Puddephat.

This forum has no comments. Why don't you add one?