Features

Features are built-in behaviours that can be customized through their configurers. Small features can be named as Configuration or Option. When they only configure an existing behaviour, such as DatabaseConfiguration, it is a Configuration. When they provide nothing to configure other than enable/disable, such as UtcOption, it is an Option.

Authentication

In this section, you will learn about how authentication is implemented in Gazel.

Authentication is enabled by default in Service Application. All requests from outside of your host project require an token. When you run your App.Service project, you will see an Authorization text box in request headers section which configures an HTTP authorization header for every request.

Below you can see a sequence diagram explaining how gazel interacts with your code through interfaces;

/-diagrams/features/authentication-sequence-diagram.mermaid

Here you can see that for every request;

  • It first calls GetSession() to retrieve session object
  • Then it invokes Validate() to enable validation logic
  • If everything is ok, it sets your session object to request scope.

ISessionManager interface

When authentication is enabled, Gazel requires an implementation of ISessionManager interface. Once you implement it in any module, it will be registered automatically.

ISessionManager is basically responsible for finding an ISession instance from a given AppToken. An example implementation is as follows;

public class SessionManager : ISessionManager
{
    ...
    public ISession GetSession(AppToken appToken) =>
        context.Query<Sessions>().SingleByAppToken(appToken);
    ...
}

If you don't want GetSession method to be public, you can implement ISessionManager explicitly.

public class SessionManager : ISessionManager
{
    ...
    private ISession GetSession(AppToken appToken) { ... }

    ...

    ISession ISessionManager.GetSession(AppToken appToken) => GetSession(appToken);
    ...
}

ISession interface

Just like a web session, ISession represents a session of a user. Unlike a web session it is not stored in cookies nor in a file. ISessionManager creates it before every request and puts it in the request scope. After every request, session instances are destroyed along with its request.

public interface ISession
{
    AppToken Token { get; } // The unique identifier of the session
    string Host { get; } // A text to represent client's host if needed
    IAccount Account { get; } // The account that this session is attached to

    void Validate(); // The method that is invoked before every request
}

Here is an example implementation of ISession interface;

public class Session : ISession
{
    private readonly IRepository<Session> repository;
    private readonly IModuleContext context;

    protected Session() { }
    public Session(IRepository<Session> repository, IModuleContext context)
    {
        this.repository = repository;
        this.context = context;
    }

    public virtual int Id { get; protected set; }
    public virtual AppToken Token { get; protected set; }
    public virtual Account Account { get; protected set; }
    public virtual string Host { get; protected set; }
    public virtual DateTime ExpireTime { get; protected set; }

    protected internal virtual Session With(Account account)
    {
        Account = account;

        Token = context.System.NewAppToken();
        Host = context.Request.Host.ToString();
        ExpireTime = context.System.Now.AddMinutes(30);

        repository.Insert(this);

        return this;
    }

    protected virtual void Validate()
    {
        if(ExpireTime < context.System.Now)
        {
            throw new AuthenticationRequiredException();
        }
    }

    //implemented explicitly to return IAccount
    IAccount ISession.Account => Account;

    //implemented explicitly to make Validate protected
    void ISession.Validate() => Validate();
}

public class Sessions : Query<Session>
{
    public Sessions(IModuleContext context) : base(context) { }

    internal Session SingleByAppToken(AppToken appToken) =>
        SingleBy(s => s.AppToken == appToken);
}

IAccount interface

IAccount represents an account in your application.

public interface IAccount
{
    int Id { get; } // Id of the account
    string DisplayName { get; } // A text to represent the account

    // The method that is invoked before every request if authorization is enabled
    bool HasAccess(IResource resource);
}

Here is an example implementation of IAccount interface;

public class Account : IAccount
{
    private readonly IRepository<Account> repository;
    private readonly IModuleContext context;

    protected Account() { }
    public Account(IRepository<Account> repository, IModuleContext context)
    {
        this.repository = repository;
        this.context = context;
    }

    public virtual int Id { get; protected set; }
    public virtual string FullName { get; protected set; }

    protected internal virtual Account With(string fullName)
    {
        FullName = fullName;

        repository.Insert(this);

        return this;
    }

    //implemented explicitly to map DisplayName on FullName
    string IAccount.DisplayName => FullName;

    //give access to everything for this sample
    bool IAccount.HasAccess(IResource resource) => true;
}

public class Accounts : Query<Account> { ... }

Accessing User Session

When you finish setting up your authentication mechanism, you can access user session using IModuleContext.Session property.

public class CompanyManager
{
    private readonly IModuleContext context;

    public CompanyManager(IModuleContext context)
    {
        this.context = context;
    }

    public Company CreateCompany(string name)
    {
        return context.New<Company>().With(
           name: name,
           createdBy: context.Session.Account.DisplayName
        );
    }
}

Overriding Current Session

You may need to override the session in some specific cases. You can use IModuleContext.OverrideSession method as shown below.

public class Company
{
    private readonly IRepository<Company> repository;
    private readonly IModuleContext context;

    protected Company() { }
    public Company(IRepository<Company> repository, IModuleContext context)
    {
        this.repository = repository;
        this.context = context;
    }

    protected internal virtual Company With(string name, string address)
    {
        //this level uses current session

        //assume we need an admin session here
        var adminSession = context.New<Session>().WithAdminRights();

        context.OverrideSession(adminSession, () =>
        {
            //here context.Session returns adminSession

            //do some admin stuff
        });

        //back to current session

        Name = name;
        Address = address;

        repository.Insert(this);

        return this;
    }
}

OverrideSession can be used nested. After each OverrideSession block ends, previous session becomes accessible.

...
protected internal virtual Company With(string name, string address)
{
    //this level uses current session
    context.OverrideSession(innerSession, () =>
    {
        //this level uses inner session
        context.OverrideSession(nestedSession, () =>
        {
            //this level uses nested session
        });
    });

    //this level uses current session
    return this;
}
...

Exception Handling

Exception handling feature provides a way for your services to respond with a code and a message for every request. To set an error code and message in your response you simply implement an exception class that extends ServiceException and throw an instance of that exception class in your business code.

But before you start writing exception classes and use them right away, we want to describe result code system in Gazel.

Result Codes

Result codes are reserved numbers starting from 0 to 99999 that will represent a response status of a service in your system. There are 5 types of result codes;

TypeAbbr.Code Range
Success-0
InfoINF1 - 10000
WarningWAR10001 - 20000
ErrorERR20001 - 90000
Fatal-99999

These types are represented by ResultCodeType enum in Gazel.Service namespace.

Result Code Blocks

There are two main concerns about result codes that need to be addressed;

  1. To be unique and static so that they can be documented
  2. To be organized so that they are easily used

There are 10K info, 10K warning codes and 70K error codes. To organize these numbers without breaking uniqueness you may split those codes into blocks, so that for every result code block there are 100 info, 100 warning and 700 error codes. Here is the table for result codes organized using result code blocks;

Block IndexInfoWarningError
#0 (Reserved)1 - 10010001 - 1010020001 - 20700
#1101 - 20010101 - 1020020701 - 21400
#2201 - 30010201 - 1030021401 - 22100
...
#999901 - 1000019901 - 2000089301 - 90000

Block 0 is reserved for built-in result codes such as 20001 - Authentication Required etc. See below section for all built-in result codes of Gazel

To create a result code block extend ResultCodeBlocks class like this;

public class ResultCodes : ResultCodeBlocks
{
    public static readonly ResultCodeBlock MyBlock = CreateBlock(1, "MyBlock");
}

You will use this class potentially from every business module, so it's better for this to be included in the most base business module.

Now that we've created a block called MyBlock at index 1, let's use this block to create a unique result code within its code ranges;

using static MyProduct.ResultCodes;
MyBlock.Info(0); // 101
MyBlock.Info(10); // 111

MyBlock.Warn(0); // 10101
MyBlock.Warn(10); // 10111

MyBlock.Err(0); // 20701
MyBlock.Err(10); // 20711

You may use one block per business module in your project. This way, errors will be organized by the domain they belong to.

Response Status

Response status is managed by Status property of IResponse to which you can access through IModuleContext.

context.Response.Status

Type of this property is IResponseStatus, which is the interface to represent all response statuses. There are 3 base classes that implements this interface;

  • ServiceInformation for information
  • ServiceWarning for warnings
  • ServiceException for errors

In service applications, ServiceInformation and ServiceWarning are included in response headers along with the successful response body. However, ServiceException is returned directly in the response body.

Implementing a response status

For every message and status extend one of the response status base classes with a proper result code.

using static MyProduct.ResultCodes;

...

public static class MyExceptions
{
    public class ThisIsWrong : ServiceException
    {
        public ThisIsWrong() : base(MyBlock.Err(0)) { }
    }
}

public static class MyWarnings
{
    public class YouAreWarned : ServiceWarning
    {
        public YouAreWarned() : base(MyBlock.Warn(0)) { }
    }
}

Here you can see that we've used a result code block MyBlock to give a proper warning code.

Throwing exceptions

When you implement an exception class that extends ServiceException you may throw it just like any .NET exception;

using static MyProduct.MyExceptions;

...

public class MyManager
{
    public void GiveMeError()
    {
         throw new ThisIsWrong();
    }
}

Exception handling mechanism catches this exception and automatically sets it as a response status.

When you throw an exception that is not a ServiceException, then it will be treated as a fatal error with result code 99999. To handle other exceptions than ServiceException you need to implement a custom exception handler as mentioned in below section.

You may still catch this exception from your business code so that exception handling mechanism is not triggered.

Setting response status

You may also directly set context.Response.Status property with an IResponseStatus instance. Below is an example of setting status to a warning;

using static MyProduct.MyWarnings;

...

public class MyManager
{
    private readonly IModuleContext context;
    public MyManager(IModuleContext context) => this.context;

    public void GiveMeStatus()
    {
        context.Response.Status = new YouAreWarned();
    }
}

Log Levels

  • Handled exceptions, responses with error codes, are logged in Warning level.
  • Unhandled exceptions, responses with fatal code , are logged in Error level.
  • Other statuses are not logged separately.

You may configure logging to only include error logs in production, so that you can monitor unhandled errors in the production code.

Localizing messages

Every result code should have a corresponding message in localization. Exception handling uses ILocalizer to retrieve message format using a key with type and code such as ERR-20001, WAR-10001. Once message format is retrieved, it is formatted using message parameters in response status object.

Let's say you defined another exception in MyBlock;

...
public class RequiredParameter : ServiceException
{
    public RequiredParameter(string parameterName) : base(MyBlock.Err(1), parameterName) { }
}

This would have a localization key ERR-20702.

Assume your localization has a message like this;

{
    "ERR-20702": "Parameter is required: '{0}'"
}

When you throw this exception;

public void CreateEmployee(string ssid)
{
    if(string.IsNullOrWhitespace(ssid))
    {
        throw new RequiredParameter(nameof(ssid));
    }
    ...
}

You will send a response with code 20702 and message;

Parameter is required: 'ssid'

Including extra data

ServiceException class has a special property called ExtraData to enable you to include extra data along with code and message so that client applications can use this information to handle some business logic in exception case.

Assume you have a Withdraw service that withdraws money from an account. You may return current balance in extra data, when withdraw operation throws insufficient balance exception so that client application can use it to provide some further functionality.

Service applications include this informations in X-Extra-Data response header. Other applications, e.g. api application, may not return this information.

Built-in Errors

Below you can see built-in exceptions and their defined error codes;

Error CodeError TypeHTTP Status Code2
20001AuthenticationRequiredException401
20002FormatException
20003PermissionDeniedException403
20004FormatException<CreditCardNumber>
20005UriFormatException
20006ObjectNotFoundException404
20007FormatException<AppToken>
20008FormatException<CardNumber>
20009FormatException<CurrencyCode>
20010FormatException<DateRange>
20011FormatException<Date>
20012FormatException<Email>
20013FormatException<MoneyRange>
20014FormatException<Money>
20015FormatException<Tckn>
20016FormatException<TimeRange>
20017FormatException<Time>
20018FormatException<Vkn>
20019InvalidEnumArgumentException
20020RangeException<DateRange>
20021RangeException<MoneyRange>
20022RangeException<TimeRange>
20023FormatException<TriState>
20024InvalidCurrencyException
20025FormatException<Rate>
20026FormatException<Timestamp>
20027FormatException<DateTime>
20028FormatException<DateTimeRange>
20029RangeException<DateTimeRange>
20030FormatException<Guid>
20031RequestIdRequiredException
20032FormatException<Binary>
20033MaxDailyRequestCountExceededException
20034FormatException<Iban>
20035FormatException<Geoloc>
20036FormatException<TimeSpan>
20037FormatException<VknOrTckn>
20038FormatException<MimeType>
20039FormatException<CountryCode>
20040NotImplementedException5011
20041FormatException<Password>
20042FormatException<EncryptedString>
20201FrameworkExceptions.Remote
20202FrameworkExceptions.InvalidSign

Information and Warnings

Result CodeResponse Status
1FrameworkInformations.Remote
10001FrameworkWarnings.ObsoleteServiceCalled
10002FrameworkWarnings.Remote
10003FrameworkWarnings.GivenLanguageCodeIsNotSupported

Customization

Exception handling mechanism is a core feature of Gazel framework. You cannot change the underlying mechanism nor you cannot disable it. However, you can extend it to provide additional exception handlers.

Creating a custom handler

Exception handling is done through IExceptionHandler interface. If you want to provide a custom mechanism to handle errors, you must implement this interface and register it to IKernel.

When an exception is thrown, exception handling feature loops through all exception handlers to find a way to convert the exception into ServiceException. If it can, then it means exception is handled with an error code, if it cannot, then it means it is an unhandled exception with the fatal code.

Below is an example of a custom exception handler for ArgumentException;

public class MyHandler : IExceptionHandler
{
    public bool Handles(Exception ex) => ex is ArgumentException;
    public ServiceException Handle(Exception ex) => new ServiceException(90001)
    ExceptionInfo GetExceptionInfo(int resultCode) =>
        resultCode == 90001
            ? new ExceptionInfo(typeof(ArgumentException))
            : null;
}

If you implement a handler inside a module project, it will be automatically registered to kernel so that exception handling can use your custom handler.


  1. This mapping violates 4xx rule for handled exceptions. This is intentional to provide you to throw a NotImplementedException anywhere without causing an error log.
  2. All handled errors are 400 unless indicated otherwise.

File System

File system feature basically enables you to read/write file content from/to a file storage.

IFileSystem interface

When an application has this feature, you can directly inject IFileSystem interface into your business objects. Below is a simple example;

public class FileManager
{
    private readonly IFileSystem file;
    public FileManager(IFileSystem file) => this.file = file;

    public await Task<Binary> FetchContent(string path)
    {
        return await file.ReadAsync(path);
    }
}

Configuring file system

Gazel has a built-in implementation that uses local storage.

services.AddGazelServiceApplication(
    ...
    fileSystem: c => c.Local(rootPath: "files"),
    ...
);

When rootPath is given, all files will be stored under this folder. You might pass an absolute path as well.

c => c.Local(rootPath: "/absolute-path"),

Local storage makes sense for development environment. You may use a network path by giving an absolute path as well.

Implementing a custom file system

When you want to store files in a different type of storage, you can implement IFileSystem interface and register it using Custom configurer in FileSystemConfigurer.

Assume you've made an S3 implementation of file system interface, you can register it as below;

c => c.Custom<S3FileSystem>()

If your implementation depends on some configuration, write a settings class, register it to the IoC and inject it to the file system implementation.

For above example, write a class named S3Settings and inject it into S3FileSystem class, and then register S3Settings to the kernel.

Configurations

Application Assemblies

You may change which assemblies are used for an application via this configuration. By default Gazel loads all assemblies starting with the root namespace of your entry assembly. Assume your Program.cs is in a project named MyCompany.MyComponent.App.Service, then the root namespace is MyCompany.

Each application configurer has a configuration named applicationAssemblies which is a lambda function that provides Assemblies instance as a parameter and expects List<Assembly> as a result. Below you can find default configuration;

services.AddGazelServiceApplication(cfg,
    ...
    applicationAssemblies: c => c.All
);

There are several ways to determine which assemblies to use;

Filtering All

You can directly filter out unwanted assemblies returned by All property.

services.AddGazelServiceApplication(cfg,
    ...
    applicationAssemblies: c => c.All.Where(a => !a.FullName.Contains("Exclude")).ToList()
);

Find By Namespace

There is a method on Assemblies class called Find that takes a namespace predicate as a parameter;

services.AddGazelServiceApplication(cfg,
    ...
    applicationAssemblies: c => c.Find(ns => ns.StartsWith("MyCompany"))
);

Find By Regex

You can also provide a namespace regex;

services.AddGazelServiceApplication(cfg,
    ...
    applicationAssemblies: c => c.Find("^MyCompany.*$")
);

Find By AssemblyType

You can make use of AssemblyType class.

services.AddGazelServiceApplication(cfg,
    ...
    applicationAssemblies: c => c.Find(AssemblyType.Module)
);

c => c.Find(AssemblyType.All) is equivalent to c => c.All

AssemblyType uses root namespace of your project that is resolved automatically. You can access your root namespace programmatically via AssemblyType.Root or Config.RootNamespace.

Application Session

TBD

Business Logic

TBD

Command Line

TBD

Database

TBD

Gateway

TBD

Http Header

TBD

Middleware

TBD

Rest Api

TBD

Service Client

TBD

Service

TBD

Options

Audit

TBD

Decimal Point

TBD

UTC

TBD