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;

sequenceDiagram
autonumber
    actor User
    participant A as Service Client
    participant B as Service Application
    participant C as Session Interceptor
    participant C1 as ISessionManager
    participant C2 as ISession
    participant D as Business Service
    User->>+A: Take action that requires Authorization
    A->>+B: Sends Request
    B->>+C: Validate Token
    C->>+C1 : Call GetSession()
    C1-->>-C : Returns ISession
    C->>+C2 : Call Validate()
    C2-->>-C : Validates Session
    C->>C : Sets ISession to Context.Session
    C-->>-B: Returns OK
    B->>+D: Forwards Request
    D-->>-B: Returns Response
    B-->>-A: Returns Response
    A-->>-User: Sees result

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;
}
...

Crypto

Crypto feature provides a basic encryption/decryption capability to store sensitive data in configuration file or database. It also provides an API to be used in accepting an input in a more secure way. Let's dive in to see how you can use this feature to make your back-end functionality more secure.

Crypto API

This API consists of two main types ICryptographer and EncryptedString providing ways to encrypt/decrypt data and represent encrypted data, respectively.

ICryptographer interface

This interface is a gateway facade to an underlying security mechanism provided by .NET. Behind the scenes, this interface uses a X.509 certificate with a an public / private key pair (for more information see X.509 and RSA). This way you can make use of asymmetric encryption where needed. There are two types of usage; Encrypt / Decrypt and Sign / Verify.

Encrypt & Decrypt

First and most basic type of usage is to encrypt data using the public key, and decrypt it using the private key. For example, you can use this to encrypt sensitive data to save it in somewhere unsafe so that you can retrieve and decrypt it later.

public class CryptoManager
{
    private readonly ICryptographer crypto;
    public FileManager(ICryptographer crypto) => this.crypto = crypto;

    public Binary GetSensitiveData()
    {
        var sensitive = "something to hide".ToUtf8Binary();

        return crypto.Encrypt(sensitive);
    }

    public bool CheckSensitiveData(Binary binary)
    {
        var sensitive = crypto.Decrypt(binary).ToUtf8String();

        return sensitive == "something to hide";
    }
}

You may share your public key with the outside world so that anyone with this key can encrypt and send a sensitive information which can only be decrypted by your back-end system.

ICryptographer also has helper methods to make string conversions easier.

...
public string GetSensitiveData()
{
    // returns base64 representation of encrypted binary
    return crypto.Encrypt("something to hide");
}

public bool CheckSensitiveData(string encryptedSensitive)
{
    return crypto.DecryptUnsecure(encryptedSensitive) == "something to hide";
}
...

Above code uses DecryptUnsecure method, which returns a regular string object. There is also a Decrypt method which returns a SecureString. SecureString is a class that encrypts given string with a symmetric key and stores in memory so that a it is safely kept in memory.

...
public bool CheckSensitiveData(string encryptedSensitive)
{
    var sensitive = crypto.Decrypt(encryptedSensitive);

    return sensitive.Decrypt() == "something to hide";
}
...

Notice that data is decrypted twice; first time it is done by using your X.509 certificate, second time it is done on SecureString object by using a symmetric key that .NET generates.

This code is for demonstration purposes only. In a real life case you might keep your sensitive data in a SecureString object, and pass it to a method as a parameter or store in a property. This way it will be less likely to be exposed. Decrypt a SecureString right before you use it in your code.

Sign & Verify

Sign & verify is like the opposite usage of encrypt & decrypt. Decryption can only happen on the side that has the private key, which is back-end side in this case. So encryption is made with a public key, decryption is made with the corresponding private key.

On the contrary, here, signing is done using a private key, and verifying is done using its corresponding public key. So in this usage it's not about hiding information from outside, rather it's about signing data to make it possible for retrievers to verify that data is signed by, thus coming from, the trusted source.

...
public string[] GetSignedData()
{
    var sensitive = "something to be signed";

    var hasher = HashAlgorithm.Create("SHA256");
    hasher.ComputeHash(sensitive.ToUtf8Binary());

    // Signing is done using the private key
    var signature = testing.Sign(hasher.Hash, "SHA256");

    return new[] { sensitive, signature };
}

public bool Verify(string[] signedData)
{
    var sensitive = signedData[0];
    var signature = signedData[1];

    var hasher = HashAlgorithm.Create("SHA256");
    hasher.ComputeHash(sensitive.ToUtf8Binary());

    // Verification is done with the public key
    return testing.Verify(hasher.Hash, "SHA256", signature);
}
...

Sign and verify is used in Secure Call feature, where response values are used to compute a hash to be signed so that client can verify and trust response.

EncryptedString struct

This struct acts like a wrapper type for SecureString class from .NET, (see SecureString). It conforms to the parseable value type convention just like rest of the value types from Gazel.Primitives package. There are three ways to create an EncryptedString instance.

  1. Parse method, when you have a plain string object;
    var encrypted = EncryptedString.Parse("something sensitive");
    
  2. FromEncrypted method, when you have an encrypted value;
    var encrypted = EncryptedString.FromEncrypted(encryptedBinary, b => crypto.DecryptUnsecure(b));
    
    Second parameter is decryptDelegate in case EncryptedString.Decrypt or Encrypted.ToString() method are called on this instance. Without a decrypt delegate it would not be able to decrpyt its value.
  3. Use a SecureString instance. You can cast it to an EncryptedString;
    var encrypted = (EncryptedString)secureString;
    
    or use constructor;
    var encrypted = new EncryptedString(secureString);
    

This struct either stores given encrypted binary value, or a secure string instance so that sensitive information is securly kept in memory. You may read How secure is SecureString section from .NET documentation to learn more.

Mapping to a Table Column

You can map EncryptedString just like any other value type.

public class Secret
{
    ...
    public virtual int Id { get; protected set; }
    public virtual string Name { get; protected set; }
    public virtual EncryptedString Value { get; protected set; }
    ...
}

If you intend to use an encrypted string property, please read below information carefully;

  • Only encrypted binary value is stored in database so that nobody can see the value without a proper certificate. So it is highly recommended for you to use a different certificate for each environment (Development, Production etc.), and never expose private key of your Production certificate to the people that have access to your production database.
  • Value is not decrypted until you explicitly call Decrypt or ToString. This is because decryption is costly and retrieving a list of persistent objects should not decrypt automatically. DataAccessLayer uses FromEncrypted method to create EncryptedString instances when data is retrieved from database.
  • Since EncryptedString is expected to store sensitive data, it will never appear in a response directly. So you don't need to make it protected internal or [Internal].

Notice how Gazel does not decrypt an EncryptedString in any case. So if your business logic somehow requires an encrypted string to be decrypted, remember that you will need to either call ToString() or Decrypt() at some point.

Below is an example of how you can generate and store a sensitive information;

public class Secret
{
    ...
    protected internal SecretToken With(string name)
    {
        Name = name;
        Value = EncryptedString.Parse($"{system.NewGuid()}");

        repository.Insert(this);

        return this;
    }
    ...
}

As a Service Parameter

You may use EncryptedString as a service parameter just like any other value type. Below you can see a simple example;

public class Secret
{
    ...
    public virtual void UpdateValue(EncryptedString value)
    {
        Value = value;
    }
    ...
}

Beware that this does not encrypt the sensitive data when transferring from client to your back-end. You can already achieve this through an HTTPS connection. EncryptedString only helps you to secure this data using a symmetric encryption when stored in memory and an asymmetric encryption when stored in database.

Although EncryptedString is accepted as a service parameter, a method that returns EncryptedString cannot be registered as a business service (a.k.a. operation), and cannot be called from outside. And also, a property with its type as EncryptedString cannot be registered in service layer as a business data as well.

When a service one of the parameters of a service is EncryptedString, that service is marked as sensitive and does not log its parameter and response data.

Reading from Settings

This feature allows you to read an encrypted string from settings using GetEncryptedString method of ICryptographer interface. Let's revisit the previous example from ICryptographer Interface section.

public Binary GetSensitiveData()
{
    var sensitive = "something to hide".ToUtf8Binary();

    return crypto.Encrypt(sensitive);
}

Here you can see that sensitive information is exposed as a constant string, which makes this sensitive information available to anyone who has access to this code. To prevent this we can read it from settings.

public Binary GetSensitiveData()
{
    var sensitive = crypto.GetEncryptedString("Sensitive");

    return sensitive.EncryptedValue;
}

Encrypt a sensitive data using your certificate and put its encrypted value to configuration file in base64 format. This way only someone with private key will be able to know actual value of sensitive data.

This method first looks for a value with given key. If value does not exist, it will look for its decrypted version under "Sensitive.Decrypted" key, so below file also works;

{
  "Sensitive": { "Decrypted": "something to hide" }
}

This can be handy in development environemnt, when sensitive data is sensitive only in production and you have some other not sensitive value in development.

Configuration

There are three ways to load a certificate, X509Store, X509CertificateFromP12File and X509CertificateFromPem. There is also a fourth, and the default, AutoResolve option which uses configuration file to decide which one to use.

X509Store

This option uses certificate store of operating system to load a certificate. If you use this option you will have to install your certificate to every OS that will run your back-end system.

Currently this option is only supported in Windows operating systems.

You can choose this option as follows;

crypto: c => c.X509Store()

X509Store requires storeName and storeLocation information to know in which store to look for a certificate. It also requires an extra search parameter to locate certificate in the specified store. By default this option looks for StoreName, StoreLocation and SubjectName in configuration file under Gazel.Certificate section like the following;

{
    "Gazel": {
        "Certificate": {
            "StoreName": "TrustedPublisher",
            "StoreLocation": "LocalComputer",
            "SubjectName": "Gazel"
        }
    }
}

If no configuration was given, storeName defaults to TrustedPublisher, storeLocation defaults to LocalComputer. Possible values to these parameters can be found at StoreName and StoreLocation pages.

You can override this configuration directly from configurer as well;

crypto: c => c.X509Store(
    storeName: StoreName.TrustedPublisher,
    storeLocation: StoreLocation.LocalMachine
)

For the extra search parameter, you may give any of search criteria defined in X509FindType. All values of X509FindType enum starts with FindBy prefix, which is removed in configuration keys. For example if you use FindByIssuerName and use Inventiv as its value, you should change configuration as follows;

{
    "Gazel": {
        "Certificate": {
            "StoreName": "TrustedPublisher",
            "StoreLocation": "LocalComputer",
            "IssuerName": "Inventiv"
        }
    }
}

You cannot override this extra search parameter from configurer like you do it with storeName and storeLocation parameters. It has to be specified from configuration file.

X509CertificateFromP12File

This option enables you to load a PKCS #12 certificate from .p12 file using configured IFileSystem instance (see File System) and suitable in non-Windows operating systems. Below code enables this option;

crypto: c => c.X509CertificateFromP12File()

It requires path and passwordFile information to load a certificate from given path and use private key with the password specified in given password file. By default it looks for Path and PasswordFile keys in configuration under Gazel.Certificate section as follows;

{
    "Gazel": {
        "Certificate": {
            "Path": "Gazel.p12",
            "PasswordFile": ".CERTPASS"
        }
    }
}

When configuration does not contain these keys, path defaults to Gazel.p12 and passwordFile defaults to .CERTPASS.

Private key password is asked to be in a separate file so that you can include a certificate, such as a development certificate in your code repository, but put .CERTPASS file in .gitignore. This will protect your certificate even if you include it in your repository.

You may override these configurations to be set directly from configurer;

crypto: c => c.X509CertificateFromP12File(
    path: "Inventiv.p12",
    passwordFile: ".Inventiv.p12.pass"
)

We suggest you to use a hidden file for password files. In a unix-like operating systems when a file starts with a dot, it means it is a hidden file (see Dot file). In Windows operating systems you explicitly need to set a file as hidden.

X509CertificateFromPem

This option enables you to give certificate information directly from configuration file or configurer. Gazel uses this option to register a stub certificate during unit test runs. Below code enables this option;

crypto: c => c.X509CertificateFromPem()

This option requires certPem, keyPem and passwordFile information to be used. By default it looks for these information configuration file using CertPem, KeyPem and PasswordFile keys, respectively, under Gazel.Certificate section. Certificate contents usually start with -----BEGIN CERTIFICATE----- and has new lines in them. To handle this you need to use \n character in json string, an example is as follows;

{
    "Gazel": {
        "Certificate": {
            "CertPem": "-----BEGIN CERTIFICATE-----\nMIIC7zCCAdegAwIBAgIQhaAixfnuiK5HBHMC+kz4zTANBgkqhkiG9w0BAQsFADAQ\nMQ4wDAYDVQQDEwVHYXplbDAeFw0xODAxMTcxMDMxMDFaFw0zOTEyMzEyMzU5NTla...",
            "KeyPem": "-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIG7xEAxkSK6gCAggA\nMAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECEmeebmmTgQsBIIEyJGTsjMc2869...",
            "PasswordFile": ".CERTPASS"
        }
    }
}

Gazel does not have a default certificate so certPem and keyPem do not have a default. Just like previous option, passwordFile defaults to .CERTPASS.

crypto: c => c.X509CertificateFromPem(
    certPem: @"
-----BEGIN CERTIFICATE-----
MIIC7zCCAdegAwIBAgIQhaAixfnuiK5HBHMC+kz4zTANBgkqhkiG9w0BAQsFADAQ
MQ4wDAYDVQQDEwVHYXplbDAeFw0xODAxMTcxMDMxMDFaFw0zOTEyMzEyMzU5NTla
...
-----END CERTIFICATE-----
",
    keyPem: @"
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIG7xEAxkSK6gCAggA
MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECEmeebmmTgQsBIIEyJGTsjMc2869
...
-----END ENCRYPTED PRIVATE KEY-----
",
    passwordFile: ".CERTIFICATE_PASSWORD"
)

Above example illustrates how you can override default configuratiion directly from configurer.

Configuring certificate directly from configurer is not recommended for production code, because it may lead you to include production certificate directly in source code. Production certificates are strongly recommended not to be in your repository. If you choose X509CertificateFromPem option, make sure you load certificate from configuration file and you don't include Gazel.Certificate section of your production environment configuration in your repository.

AutoResolve

This option is the default option for crypto feature and enables you to configure crypto feature completely through configuration file. You don't have to select this option explicitly but you can still choose to do it as shown below;

crypto: c => c.AutoResolve()

It does not accept any parameters. Default behaviour is to use X509Store on Windows and X509CertificateFromP12File on unix-like operating systems. To explicitly specify which option to use, you can add Type key under Gazel.Certificate section;

{
    "Gazel": {
        "Certificate": {
            "Type": "X509Store"
        }
    }
}

Option names are the same as previous sections; X509Store, X509CertificateFromP12File and X509CertificateFromPem. Each option expects its own keys as explained in previous sections. Below example uses store option and specifies certificate information under same conffiguration section;

{
    "Gazel": {
        "Certificate": {
            "Type": "X509Store",
            "SubjectName": "Inventiv"
        }
    }
}

Combining options is also possible. Assume some people use Windows for development and some use unix-like systems. Below example will be sufficient to configure both at one place;

{
    "Gazel": {
        "Certificate": {
            "SubjectName": "Inventiv",
            "Path": "Inventiv.p12",
        }
    }
}

This configuration will look for a certificate with subject name Inventiv in trusted publisher and local machine for Windows operating systems, and will look for a certificate Inventiv.p12 in file system for unix-like operating systems.

Configuring certificates for a Gazel CLI process

Gazel CLI allows you to generate a schema from a bin directory. When you use this option Gazel CLI will render given bin directory including your configuration files and configurers in your Program.cs. This means that if you use crypto feature, especially in configuration files, it might require a correct configuration for crypto feature as well.

Gazel CLI uses Development for environment by default. However if you face a problem while generating schema, code or dll because of encryption problems, you can make use of --environment option to set environment during Gazel CLI process.

g schemagen bin/net6.0/Debug --environment GazelCLI

This, for example, sets environment to GazelCLI so that you can create a appsettings.GazelCLI.json file and specify your certificate option in this configuration under Gazel.Certificate section.

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);
    }
}

Configuration

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

builder.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.

Customization

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;

builder.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.

builder.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;

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

Find By Regex

You can also provide a namespace regex;

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

Find By AssemblyType

You can make use of AssemblyType class.

builder.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