Conventions
In this section you will learn about the conventions in Gazel. Keep in mind that Gazel heavily relies on Convention over Configuration design paradigm.
- Dependency Injection : Learn to use how to make use of dependency injection in Gazel
- Business Services : Find out how a public method turns into a business service
- Data Persistence : Understand conventions for data persistence
- Relations : Explore different ways to create relations between persistent classes
- Queries : Learn how to create query services
- Transaction Management : Learn about how Gazel manages transactions
- Exception Handling : See how to write exceptions in Gazel
Dependency Injection
Gazel uses Castle.Windsor for Dependency Injection. Using Convention over Configuration capabilities of Castle.Windsor, you don't have to register your classes one by one. Every public class in module projects are automatically registered to the kernel. In this document you will find ways on how to make use of this feature.
Scopes
There are 3 types of scope;
- Transient: For every resolution, Windsor creates a new instance.
- Request: For every request there is only one instance. After the request ends, object is disposed.
- Singleton: There is only one instance until application is shut down.
Manager
and Query
classes are singleton by convention and the rest are
transient.
Manager classes
For a class to be a manager class, it should have Manager
suffix.
public class ProductManager{ ...}
Manager classes are singleton by convention. You can make use of Manager
classes to provide general domain logic of a module, batch operations or
complex query services.
Injecting a Dependency
Every public class is registered to Castle.Windsor by their own type so that they can be injected to other classes.
public class ProductManager{ private readonly UserManager _userManager; public ProductManager(UserManager userManager) { _userManager = userManager; } ...}public class UserManager { ... }
Public Constructors
Every public class should contain a public
constructor, in order for
Castle.Windsor to register and configure them.
public class ProductManager{ ... public ProductManager() { ... } ...}
Circular Dependencies
Avoid creating circular dependencies as shown below;
public class ProductManager{ ... public ProductManager(UserManager userManager) { ... } ...}public class UserManager{ ... public UserManager(ProductManager userManager) { ... } ...}
For such cases;
- Consider refactoring your code. Two classes shouldn't depend on each other.
- If you think you have no other choice, then you can refactor code as shown below;
public class UserManager{ private readonly IModuleContext _context; public UserManager(IModuleContext context) { _context = context; } private ProductManager ProductManager { get { return _context.Resolve(typeof(ProductManager), Scope.Any); } } ...}
IModuleContext
interface
IModuleContext
is basically an abstraction for your domain objects to be
isolated from the 3rd party libraries.
For singleton objects, mainly Manager
objects, we encourage you to inject
them through constructor. On the other hand, if you want to create a transient
object, you can make use of IModuleContext.New<T>()
.
public class ProductManager{ private readonly IModuleContext _context; public ProductManager(IModuleContext context) { _context = context; } public void DoSomethingHeavy() { _context.New<BatchUserOperation>().DoSomethingHeavy(); } ...}public class BatchUserOperation{ private readonly UserManager _userManager; public BatchUserOperation(UserManager userManager) { _userManager = userManager; } internal void DoSomethingHeavy() { ... }}
Injecting Interfaces
Public classes can also be injected using their interfaces
public class ProductManager{ private readonly IUserManager _userManager; public ProductManager(IUserManager userManager) { _userManager = userManager; } ...}public interface IUserManager { ... }public class UserManager : IUserManager { ... }
Injecting Multiple Implementations
If an interface has more than one implementations then you can inject all of
them at once by using IList<T>
.
public class ProductManager{ private IList<INotifier> _notifiers; public ProductManager(IList<INotifier> notifiers) { _notifiers = notifiers; } ...}public interface INotifier { ... }public class SmsNotifier : INotifier { ... }public class MailNotifier : INotifier { ... }public class PushNotifier : INotifier { ... }
Business Services
Gazel configures Routine to expose every public method to be a business service. A business service is a public method that you can call using HTTP.
Public Methods
public
methods are directly exposed as business services.
public class Company{ ... public virtual List<Branch> GetBranches() { return _context.Query<Branches>().ByCompany(this); } ...}
Persistent Objects as Parameters
You can use a persistent object directly in your business methods. Gazel will
automatically lookup for a record with given id. If record is not found, it
will automatically throw ObjectNotFoundException
.
public class Company{ ... public virtual Branch AddBranch(string name, District district) { return _context.New<Branch>().With(this, name, district); } ...}
Therefore, with the above code, you ensured that district is either null or an existing record in database.
Method Overloads
Method overloads are considered as one business service in service layer. If you create method overloads like below, then you will see only one business service with all of parameters.
public class Company{ ... public virtual Branch AddBranch(string name, District district) => AddBranch(name, district, null); public virtual Branch AddBranch(string name, District district, Company ownerCompany) { ... } ...}
If you don't send an ownerCompany
then the first overload will be called, if
you fulfill all parameters, then the second overload will be called.
Overload selection is automatic, and Gazel will try to pass as many
parameters as it can. Assume that you send name
and ownerCompany
to above
AddBranch
service. Then first overload matches only name
parameter, but
second overload matches both of them. So invocation will be on the second one
and district
parameter will be null
.
If return type of overloads are not the same, the first overload will be a business service, but the second one will be ignored.
public class Company{ ... // First overload returns void public virtual void AddBranch(string name, District district) { ... } // Second overload returns Branch, this will not be a business service. public virtual Branch AddBranch(string name, District district, Company ownerCompany) { ... } ...}
Optional parameters
Gazel supports optional parameters in business services. You can use optional parameters instead of overloads.
public class Company{ ... public virtual Branch AddBranch(string name, District district, Company ownerCompany = null ) { ... } ...}
List
and array
parameters and return types
You can use List
class for both parameters and return types as long as type
parameter T
for lists is a supported type.
public class Company{ ... public virtual List<Branch> GetBranches() { ... } public virtual Employee[] GetEmployees() { ... } ...}
No Dictionary
parameters and return types
Gazel does not support Dictionary
in business services by design. So any
service with Dictionary parameter or return type will not be exposed as a
business service. The reason behind this design decision is to favor
strongly-typed classes over generic dictionaries.
Data Transfer Objects (DTOs)
If you need a type other than persistent class or a primitive for input or output, then you can make use of DTOs.
public class Company{ ... public virtual void AddBranch(string name, string address, City city, Country country) { ... } ...}
Example usage of a DTO with business service;
public class Company{ ... public virtual void AddBranches(List<BranchInfo> branches) { ... } ...}
By definition DTOs are expected to be only used for data transfer with no
business logic and for this reason Gazel uses record
.
You can define a record with using positional syntax. With this syntax, compiler generates your record as a class with init only public properties and a public constructor.
public record BranchInfo(string Name, string Address, City City, Country Country);
Constructor parameters of record DTOs are just like parameters of business services. You can use
- primitives and other value types,
- persistent classes,
- other DTOs as parameters of constructor.
You can use constructor overloads if you need some construction logic or validation when creating your DTOs.
Example usage of a record with a constructor overloads.
public record BranchInfo(string Name, string Address, City City, Country Country){ public Geoloc Location { get; init; } public BranchInfo(string name, Geoloc location) : this(name, default, default, default) { Location = location; }}
You can use optional or calculated properties in records, without adding a constructor overload.
public record BranchInfo(string Name, City City, Country Country, string Address = default){ ... public string FullAddress => $"{Name} - {Address} {City} {Country}"; ....}
If you need to keep some properties or methods only for internal business
logic, you can use internal
access modifier or [Internal]
attribute.
public record BranchInfo(string Name, [property: Internal] string Address,...){ // this constructor will not be rendered in client APIs, // it will only be available for your business services internal BranchInfo(Branch branch) : this(branch.Name, branch.Address, branch.City, branch.Country) { } public string FullAddress => $"{Name} - {Address} {City} {Country}";}
Example usage of record with internal constructor with business service;
public class Company{ ... public virtual List<BranchInfo> GetBranches() { return _context .Query<Branches>() .ByCompany(this) .Select(b => new BranchInfo(b)) .ToList() ; } ...}
Legacy
struct
DTOs are currently supported as legacy for backward compatibility
and may not be available on future releases. We recommend you to use
record
over struct
in your new projects.
Below is a struct implementation of BranchInfo record.
public record BranchInfo(string Name, string Address, City City, Country Country);
public struct BranchInfo{ public string Name { get; private set; } public string Address { get; private set; } public City City { get; private set; } public Country Country { get; private set; } public BranchInfo(string name, string address, City city, Country country) : this() { Name = name; Address = address; City = city; Country = country; }}
Hiding Public Methods and Properties
In some cases you might need to have a public method, but you might not want it
to be exposed as a business service. In this kind of cases, you can mark a
business service by adding an Internal
attribute on top of a class
,
constructor
, method
or property
like below;
[Internal]public class InternalManager{ public PublicData ShouldNotBeRegisteredAsAService() { return new PublicData("public internal", "public"); }}public class PublicManager{ private readonly InternalManager _internalManager; public PublicManager(InternalManager internalManager) { _internalManager = internalManager; } public PublicData PublicService() { return _internalManager.ShouldNotBeRegisteredAsAService(); } [Internal] public void InternalService() { }}public record PublicData([property: Internal] string InternalProperty, string PublicProperty);
For the above example;
InternalManager
class and all of its members will be hidden in service layer.PublicManager.InternalService
method will be hidden in service layer.- For
PublicData
class;- The constructor will be hidden,
InternalProperty
will be hidden,- But
PublicProperty
will be available in service layer
You can think of this attribute as an additional access modifier to public
,
private
, internal
, protected
and protected internal
access modifier
which we sometimes refer as public internal
.
Data Persistence
Gazel configures NHibernate with the help of Fluent NHibernate to provide a data persistence layer.
Persistent Class Conventions
An object is Persistent
if its class injects its own repository. This
convention ensures that there is only one class to deal with one table in the
database.
public class Company{ private readonly IRepository<Company> _repository; protected Company() { } public Company(IRepository<Company> repository) { _repository = repository; } ...}
This injection tells Gazel to configure this class to be a persistent class which means that there will be an ORM configuration for this class.
protected Task() { }
constructor is there for NHibernate to be able to
create proxies on persistent classes for lazy loading.
Query classes
When you have a persistent class, this means that you will need to read persistent objects from corresponding database table. That's why Gazel requires you to create a query class for every persistent class.
public class Company{ ...}public class Companies : Query<Company>{ ...}
It's better to put persistent and query classes of a table into one source file.
In this document we will explain persistent classes rather than query classes. You can find detailed explanation for queries in this page .
Id property
Every persistent class should have an identifier property of type int
and of
name Id
.
public class Company{ ... public virtual int Id { get; protected set; } ...}
This property will be used by NHibernate to be the primary key and first level caching.
Id properties are automatically configured to be an identity column which means that database is responsible for assigning Id values.
virtual
keyword
Every member in persistent classes (methods and properties) should be virtual
in order NHibernate to be able to create proxies for lazy loading.
public class Company{ ... public virtual int Id { get; protected set; } public virtual string Name { get; protected set; } public virtual string Address { get; protected set; } ...}
In persistent classes private
access modifier causes null reference
exceptions. Use protected virtual
instead of private
to workaround this
problem.
protected
setters
We encourage you to use protected setters so that you can make sure that no other class than the class itself is able to modify the values of its properties.
It could be private
setters but NHibernate wouldn't be able to create
proxies for lazy loading.
For a more detailed explanation please have a look at this tutorial page .
Inserting a New Record
There is an Insert
method in IRepository<T>
which does what it says. The
convention for insert operations is as follows;
public class Company{ ... protected internal virtual Company With(string name, string address) { Name = name; Address = address; _repository.Insert(this); return this; } ...}
With
methods are part of the creation of a persistent object. We use builder
pattern for this operation. For a more detailed explanation about With
methods please have a look at this tutorial
page .
With
methods are;
protected
andvirtual
because of NHibernate's lazy loading requirements,internal
to be able to use from other classes in its module.
Below you can see an example of an insert operation;
public class CompanyManager{ ... public void CreateCompany(string name, string address) { _context.New<Company>().With(name, address); } ...}
Updating a Record
When a persistent object is loaded, its state is managed by NHibernate. This feature of NHibernate enables services to automatically update a record upon commit.
public class Company{ ... public virtual void Update(string name, string address) { Name = name; Address = address; } ...}
Above method is enough for the Company
object to update itself. At this point
object is marked as dirty by NHibernate. Whenever NHibernate session is
flushed, an update statement will be executed in the database. There are 3
reasons for NHibernate session to be flushed;
- When current transaction is committed NHibernate flushes current session, hence record gets updated.
- When a select query is sent to
Company
table, either directly or via table joins. - You make an explicit call to
IRepository<T>.Flush()
which calls NHibernate's session flush directly.
IRepository<T>.Flush()
causes all dirty objects to be flushed, not just the
persistent object it is called, nor instances of <T>
. This is how
NHibernate implements session flush.
Batch Updates
public class Company{ ... public virtual bool Active { get; protected set; } ... public virtual void Deactivate() { Active = false; }}public class CompanyManager{ ... public virtual void DeactivateCompanies(string name) { foreach(var company in _context.Query<Companies>().ByName(name)) { company.Deactivate(); } } ...}
DeactivateCompanies
method iterates through a list of Company
objects.
Changing a property value does not cause an immediate update. Upon NHibernate
session flush, above updates will be executed as batch.
If you execute a query to Company
table inside Deactivate
method, this
will cause NHibernate to flush on every iteration and cause an update
execution one by one. This can create a performance flaw in your code. If you
need to check something before such an update (e.g. checking if new username
exists before updating it), and if you want the operation to be a batch
update, then you need to optimize your query accordingly. For "unique
username" example, you might consider checking uniqueness at once.
public class User{ ... public virtual bool Username { get; protected set; } ... public virtual void ChangeUsername(string username) { ChangeUsername(username, false); } protected internal virtual void ChangeUsername(string username, bool batch) { if(!batch) { if(_context.Query<Users>().CountByUsername(username) > 0) { throw new ArgumentException("username"); } } Username = username; } ...}public class UserManager{ ... public virtual void ChangeUsernames(List<UsernameChange> changes) { if(_context.Query<Users>().CountByUsernames(changes.Select(c => c.To)) > 0) { throw new ArgumentException("changes"); } var users = _context.Query<Users>().ByUsernames(changes.Select(c => c.From)); foreach(var user in users) { users.ChangeUsername(change.To, true); } } ...}
Force Update
If a persistent class implements IAuditable
, this means that with every
update to objects of that class, Gazel will automatically update values of
AuditInfo
properties. A force update is handy when you only want to update
AuditInfo
properties (e.g. ModifyDate
). You can force an object to be
updated using ForceUpdate
method of IRepository<T>
.
public class Company : IAuditable{ ... public virtual AuditInfo AuditInfo { get; protected set; } ... public virtual void AddEmployee(string name) { _context.New<Employee>().With(this, name); _repository.ForceUpdate(this); } ...}public class Companies : Query<Company> { ... }
In the above example, we want a company record to be updated whenever an
employee is added to it. Since adding an employee does not cause an update to a
company record, we force company objects to be updated upon adding an employee.
This ForceUpdate
call will cause a company object's AuditInfo.ModifyDate
column to be refreshed with AddEmployee
operation.
If given persistent class does not implement IAuditable
interface, an
InvalidOperationException
will be thrown.
If a persistent object is already dirty and forced to be updated, there will
be only one UPDATE
statement to update changes to database.
Deleting a Record
Deleting is done by Delete
method in IRepository<T>
.
public class Company{ ... public virtual void Delete() { _repository.Delete(this); } ...}
Batch Deletes
Behaviour of delete operations are similar to update operations. If you do it like above, delete statements will be executed when NHibernate session is flushed. If you execute a query before deleting, this would prevent NHibernate from performing a batch operation. For detailed explanation have a look at Batch Updates section above in this document.
Relations
Gazel configures NHibernate so that you can define relations intiutively.
Many-to-One Relation
This relation is the most used type among all and the simplest one.
public class Company{ ... public virtual int Id { get; protected set; } public virtual string Name { get; protected set; } ...}public class Employee{ ... public virtual int Id { get; protected set; } public virtual Company Company { get; protected set; } public virtual string Name { get; protected set; } ...}
Above code is enough to set a many-to-one relation between Employee
and
Company
classes. In this example, Employee
table will require a column
named CompanyId
that is mapped to Company
property as a many to one
relation.
One-to-Many Relations
Creating List<T>
properties on persistent classes will NOT cause a
one-to-many relation. To have this relation, simply create a method that
queries child records from the parent record;
public class Company{ ... public virtual List<Employee> GetEmployees() { return _context.Query<Employees>().ByCompany(this); } ...}public class Employees : Query<Employee>{ ... internal List<Employee> ByCompany(Company company) { return By(e => e.Company == company); } ...}
Eager-Fetching and Lazy-Loading
Gazel configures NHibernate to eager fetch persistent object with one level. This means that when you query a list of persistent objects, their parents will be fetched eagerly using an inner join. But their grandparents will be lazy-loaded.
public class Company{ ... public virtual string Name { get; protected set; } ...}public class Department{ ... public virtual string Name { get; protected set; } public virtual Company Company { get; protected set; } ...}public class Employee{ ... public virtual string Name { get; protected set; } public virtual Department Department { get; protected set; } ...}
When you query employees with something like
_context.Query<Employees>().ByName("mike")
, the resulting Employee
objects
will have Department
objects eagerly fetched. However, when you try to access
a property of their Company
object (other than Id property), a query will be
executed using that Company
object's primary key.
public class CompanyManager{ ... public void SomeEmployeeOperation(string name) { var employees = _context.Query<Employees>().ByName(name); foreach(var employee in employees) { var departmentName = employee.Department.Name; //No query is executed, department is already loaded var companyName = employee.Department.Company.Name; //A query is executed to load company object from database } } ...}
About N + 1 select problem
Above code causes N+1 select problem, which can be a performance issue. If you
encounter this problem, you need to consider selecting all grandparents
(Company
) of the children (Employee
) with an additional query so that there
will be only 2 queries in total.
public class CompanyManager{ ... public void SomeEmployeeOperation(string name) { var employees = _context.Query<Employees>().ByName(name); var companies = _context.Query<Companies>().ByIds(employees.Select(e => e.Department.Company.Id)); foreach(var employee in employees) { var departmentName = employee.Department.Name; //No query is executed, department is already loaded var companyName = employee.Department.Company.Name; //No query is executed, company is already loaded with the second query. } } ...}
One-to-Any Relation
This relation type enables you to map your properties to interfaces, which we refer to as Interface Mapping or Polymorphic Mapping.
public class Order{ ... public virtual ICustomer Customer { get; protected set; } ...}
For NHibernate to map this property to a table it needs two columns;
CustomerId
: The id value of related record, like in a Many-to-One relation,CustomerType
: Type of related record.
NHibernate uses type column to know which table to select. This type
information is retrieved from an enum that corresponds to IOrderProcessor
interface.
public interface ICustomer{}public enum CustomerType{ RealPerson = 1, LegalEntity = 2}public class RealPerson : ICustomer { ... }public class LegalEntity : ICustomer { ... }
Conventions for one-to-any mapping are;
- For an interface named
I[Name]
(e.g.ICustomer
) - There should be an enum named
[Name]Type
(e.g.CustomerType
) - Each enum member must be a persistent class and implement
I[Name]
interface (e.g.RealPerson
andLegalEntity
). - When
I[Name]
is mapped to a persistent class with a property named[Property]
,[Property]Id
(CustomerId
) column stores the id value of the related record[Property]Type
(CustomerType
) column store enum member values (1
or2
), not member names (RealPerson
orLegalEntity
)
In a query method, you can filter by object like this;
public class Orders : Query<Order>{ ... internal List<Order> ByCustomer(ICustomer customer) { return By(wa => wa.Customer == customer); //uses both CustomerId and CustomerType columns } ...}
Or you can filter by type like this;
public class Orders : Query<Order>{ ... internal List<Order> ByCustomerType<T>() where T : ICustomer { return By(wa => wa.Customer is T); //uses only Customer column } ...}
Unfortunately interface mappings cannot be fetched eagerly. So beware of N+1 select problems if you use this feature.
Queries
For persistence classes, there needs to be corresponding query class to read records from database. This section focuses on how you can organize your queries in your projects.
Query Class Conventions
The main purpose of query classes is to organize all of the queries of a table together into one place. Query classes are named after their corresponding persistent class. They are in plural form like below;
public class Company { ... }public class Companies : Query<Company>{ ...}
You can access pluralization service through IModuleContext.Pluralizer
property.
If pluralization service does not provide what you want, you can make use of
[Name]s
convention. For instance, there is a persistent class named Xyz
,
when you name its query class as Xyzs
, it will work as well.
Query classes are singleton by convention and its usage is as follows;
public class CompanyManager{ ... public virtual void DeactivateCompanies(string name) { foreach(var company in _context.Query<Companies>().ByName(name)) { company.Deactivate(); } } ...}
Query classes are singleton by convention, so you can inject query classes as well;
public class CompanyManager{ private Companies _companies; public CompanyManager(Companies companies) => _companies = companies; public virtual void DeactivateCompanies(string name) { foreach(var company in _companies.ByName(name)) { company.Deactivate(); } }}
Query classes should extend Query<T>
which is an abstract class with helper
functionalities to make it simple to implement query methods. To provide these
functionalities it requires IModuleContext
to be injected.
public class Company { ... }public class Companies : Query<Company>{ public Companies(IModuleContext context) : base(context) { } ...}
If you want to inject other dependencies, you are free to do it like in any other class.
By Methods
By methods are type of queries that return list of persistent objects.
public class Company { ... }public class Companies : Query<Company>{ ... public List<Company> By(string name, City city) => By(c => c.Name.Contains(name) && c.City == city); ...}
By
method is declared in Query<T>
base class to help you create query
methods quickly.
Query<T>.By
method accepts an expression that is converted to SQL
. This
expression never runs in your .NET application. Because of this reason you
are not supposed to call methods of persistent classes within these
expressions. For example; By(c => c.GetEmployees().Count > 0)
will not
work.
Do not use Id
properties for filtering;
public class Company { ... }public class Companies : Query<Company>{ ... public List<Company> By(string name, City city) => By(c => c.Name.Contains(name) && c.City.Id == city.Id); ...}
This will cause a NullReferenceException
when city is null
. Prefer
c.City == city
expression, which will handle both cases in one shot.
A by method can also be implemented like below;
public class Company { ... }public class Companies : Query<Company>{ ... public List<Company> By(string name, City city) => Lookup.List(true).Where(c => c.Name.Contains(name) && c.City == city).ToList(); ...}
Lookup
property is declared in Query<T>
base class and is of type
ILookup<T>
. This interface acts as a gateway to NHibernate. ILookup<T>.List
method returns an IQueryable<T>
instance. You may use this instance when By
method is not enough.
Single Parameter Convention
When there is only one parameter in query methods, we suggest you to include parameter name in method name like below;
public class Transaction { ... }public class Transactions : Query<Transaction>{ ... public List<Transaction> ByFrom(Account from) => By(t => t.From == from); ...}
Consider you have two different queries on Transaction
table, first one
filters using From
column, second one filters using To
column;
public class Transaction { ... }public class Transactions : Query<Transaction>{ ... public List<Transaction> By(Account from) => By(t => t.From == from); public List<Transaction> By(Account to) => By(t => t.To == to); ...}
Above code will not compile because there are two methods with exactly the same signature. To make it compile, you need to rename one of them. We prefer renaming both to provide consistency in naming;
public class Transaction { ... }public class Transactions : Query<Transaction>{ ... public List<Transaction> ByFrom(Account from) => By(t => t.From == from); public List<Transaction> ByTo(Account to) => By(t => t.To == to); ...}
Take and Skip
You can use skip
and take
extensions methods. They are optional parameters.
take
parameter extracts the first n elements from the beginning of the target
sequence. Here is how you can do it;
public class Company { ... }public class Companies : Query<Company>{ ... public List<Company> By(string name, City city, int take) => By(c => c.Name.Contains(name) && c.City == city, take: take); public List<Company> All(string name, City city, int take) => All(c => c.Name.Contains(name) && c.City == city, take: take); ...}
The skip
parameter moves pass the first n elements from the beginning of the
target sequence, returning the remainder;
public class Company { ... }public class Companies : Query<Company>{ ... public List<Company> By(string name, City city, int skip) => By(c => c.Name.Contains(name) && c.City == city, skip: skip); public List<Company> All(string name, City city, int skip) => All(c => c.Name.Contains(name) && c.City == city, skip: skip); ...}
You can apply pagination by using skip
and take
optional parameters. Here
is how you can do it;
public class Company { ... }public class Companies : Query<Company>{ ... public List<Company> By(string name, int skip, int take) => By(c => c.Name.Contains(name), skip: skip, take:take); public List<Company> All(string name, int skip, int take) => All(c => c.Name.Contains(name), skip: skip, take:take); ...}
Alternatively, you can also be implemented by using ILookup<T>.List
as shown
below. It actually returns an IQueryable<T>
instance.
public class Company { ... }public class Companies : Query<Company>{ ... public List<Company> By(string name, City city, int take) => Lookup .List(true) .Where(c => c.Name.Contains(name) && c.City == city) .Take(take) .ToList();; public List<Company> By(string name, City city, int skip) => Lookup .List(true) .Where(c => c.Name.Contains(name) && c.City == city) .Skip(skip) .ToList();; public List<Company> By(string name, City city, int take, int skip) => Lookup .List(true) .Where(c => c.Name.Contains(name) && c.City == city) .OrderByDescending(c => c.City) .Skip(skip) .Take(take) .ToList(); ...}
OrderBy and OrderByDescending
You can use orderBy
and orderByDescending
extensions methods. They are
optional parameters.
You can use orderBy
and orderByDescending
parameters in By
, All
,
FirstBy
, methods.
The orderby
parameter sorts the elements of a sequence in ascending order
according to a key.
public class Company { ... }public class Companies : Query<Company>{ ... public List<Company> ByName(string name) => By(c => c.Name.StartsWith(name), orderBy: c => c.Name); public List<Company> All() => All(orderBy: c => c.Name); public List<Company> FirstByName(string name) => FirstBy(c => c.Name.StartsWith(name), orderBy: c => c.Name); ...}
The orderByDescending
parameter sorts the elements of a sequence in
descending order according to a key.
public class Company { ... }public class Companies : Query<Company>{ ... public List<Company> ByName(string name) => By(c => c.Name.StartsWith(name), orderByDescending: c => c.Name); public List<Company> All(string name) => All(orderByDescending: c => c.Name); public List<Company> FirstByName(string name) => FirstBy(c => c.Name.StartsWith(name), orderByDescending: c => c.Name); ...}
FirstBy and SingleBy
FirstBy
and SingleBy
methods are like By
methods but they return only one
record. FirstBy
returns the first record matching the given conditions,
whereas SingleBy
throws an exception when there are more than one records
matching the given conditions.
public class User { ... }public class Users : Query<User>{ ... public User SingleByEmail(Email email) => SingleBy(u => u.Email == email); public User FirstByRegistrationDate(Date registrationDate) => FirstBy(u => u.RegistrationDate == registrationDate); ...}
Single Parameter Convention applies to all query methods. That's why above
methods are named as SingleByEmail
and FirstByRegistrationDate
instead of
SingleBy
and FirstBy
. When you have more than one parameter you may
exclude parameter name from method name (e.g. public User SingleBy(Email email, Password password)
).
SingleBy
method will return null
when there are no matching records. LINQ
extension methods uses a different convention. Single
methods expects to
return one record, if there are none or more than one they will throw an
exception. SingleBy
methods acts like SingleOrDefault
method.
CountBy
As the name implies, CountBy
methods executes a count
query and returns an
int
.
public class User { ... }public class Users : Query<User>{ ... public int CountByRegistrationDate(Date registrationDate) => CountBy(u => u.RegistrationDate == registrationDate); public int CountBy(Gender gender, Date birthDate) => CountBy(u => u.Gender == gender && u.BirthDate == birthDate); ...}
AnyBy
As the name implies, AnyBy
method determines whether all elements of a
sequence satisfy a condition and returns a bool
.
public class User { ... }public class Users : Query<User>{ ... public bool AnyByRegistrationDate(Date registrationDate) => AnyBy(u => u.RegistrationDate == registrationDate); public bool AnyBy(Gender gender, Date birthDate) => AnyBy(u => u.Gender == gender && u.BirthDate == birthDate); ...}
MinBy and MaxBy
These aggregate functions take two expressions.
- An expression of the property on which aggregate function is applied
- An expression for where clause
public class Transaction { ... }public class Transactions : Query<Transaction>{ ... public Money MinAmountBy(DateRange transactionDateRange, CurrencyCode currency) => MinBy( //Property expression t => t.Amount.Value, //Where clause expression t => t.TransactionDate >= transactionDateRange.Start && t.TransactionDate < transactionDateRange.End && t.Amount.Currency == currency ).ToMoney(currency); public Money MaxAmountBy(DateRange transactionDateRange, CurrencyCode currency) => MaxBy( //Property expression t => t.Amount.Value, //Where clause expression t => t.TransactionDate >= transactionDateRange.Start && t.TransactionDate < transactionDateRange.End && t.Amount.Currency == currency ).ToMoney(currency); ...}
Optional Where Clauses
In a query class, if a condition needs to be included in a query depending on the state of a given parameter, an optional where clause can be created as shown below;
public List<Company> By(City city, string name = default, Vkn taxNo = default){ return By(c => c.City == city, When(name).IsNot(default).ThenAnd(c => c.Name.StartsWith(name)), When(taxNo).IsNot(default).ThenAnd(c => c.TaxNo == taxNo) );}
This way you can create reusable query services/methods;
companies.By(city);companies.By(city, taxNo: taxNo);companies.By(city, name: name, taxNo: taxNo);
You can use optional where clauses in By
, FirstBy
, SingleBy
, MinBy
,
MaxBy
, CountBy
methods.
An optional where clause is built in 3 steps:
When
: In this step you specify the parameter on which you will check a condition.Is/Not
:Is
method expects the given condition to betrue
whileIsNot
method expects the given condition to befalse
.ThenAnd
: This is the final step. In this step you provide the where clause.
When(name).IsNot(default).ThenAnd(c => c.Name.StartsWith(name))
Together, in the above statement, you stated that there is an optional filter
which should be included when name is not default
.
The alternative, you can also use as named optional parameters. They must be the last ones in method arguments list.
There are available 2 different ways.
optional
: In this way, you specify the single condition.optionals
: In this way, you specify the multiple conditions.
You can pass the parameter according to the name, as shown below;
...public List<Company> By(City city, string name = default){ return By(c => c.City == city, optional: When(name).IsNot(default).ThenAnd(c => c.Name.StartsWith(name)) );}public List<Company> By(City city, string name = default, Vkn taxNo = default){ return By(c => c.City == city, optionals: new[] { When(name).IsNot(default).ThenAnd(c => c.Name.StartsWith(name)), When(taxNo).IsNot(default).ThenAnd(c => c.TaxNo == taxNo) } );}...
How to use Is
//if given name parameter object is expectedName//then the condition will be included in the query.When(name).Is(n => n == expectedName)//you can pass an object instead of an expression//which will be equivalent to above codeWhen(name).Is(expectedName)//There is a shortcut method that does the same job//that When(name).Is(null) and When(name).Is(default) doesWhen(name).IsDefault()
How to use IsNot
//if given name parameter object is not excludedName//then the condition will be included in the query.When(name).IsNot(c => c == excludedName)//you can pass an object instead of an expression//which will be equivalent to above codeWhen(name).IsNot(excludedName)//There is a shortcut method that does the same job//that When(name).IsNot(null) and When(name).IsNot(default) doesWhen(name).IsNotDefault()
Query Base Class
Query<T>
is an abstract class with helper functionalities to make it simple
to create queries. All methods in this class are protected
. If a method
declared in Query<T>
class is to be exposed as a business service, you can
override its access modifiers with public new
keyword.
SingleById and ByIds
public abstract class Query<T> : IQuery<T>{ ... protected virtual T SingleById(int id) { ... } protected virtual List<T> ByIds(List<int> ids) { ... } ...}
These methods are protected helper methods and are used by Gazel to find a record by an id or ids. If you want these methods to be available for internal use, then you can simply do the following;
public class Company { ... }public class Companies : Query<Company>{ ... internal new Company SingleById(int id) { return base.SingleById(id); } ...}
SingleById
caches the result in request scope, that is, when you make
subsequent calls to SingleById
, only first call will hit database.
Query.All
This method lists all records in corresponding table. This method is
protected
in Query<T>
base class. If you want a persistent class to have an
All
query as a business service do the following;
public class Company { ... }public class Companies : Query<Company>{ ... public new Company All() { return base.All(); } ...}
Transaction Management
Business services always runs in a transaction context. Its either main transaction or manually created transactions. In this section you can learn how we deal with database transactions.
Using Main Transaction
Before every service call, Gazel creates a database connection and begins a transaction. If there occurs an exception the transaction is automatically rolled back. If there are no exceptions than transaction is committed.
public class Company{ ... public virtual void AddEmployee(string name) { _context.New<Employee>().With(this, name); throw new Exception(); //Above insert will be rolled back } ...}
How to Disable Main Transaction
If you want to disable this main transaction behaviour, simply put
[ManualTransaction]
attribute to the service method. This will prevent a
transaction to be opened.
public class Company{ ... [ManualTransaction] public virtual void AddEmployee(string name) { ... } ...}
When you use [ManualTransaction]
you are not allowed to use a persistent
object as a parameter because there will be no transaction or connection to
fetch that persistent object.
Disabling main transaction can help when you need to make an external or internal API call before you make any database operation. Normally, when you make an API call, you left a connection and a transaction open and waiting in an idle state. If your service requires an external call, we suggest you to disable main transaction and make your external calls without blocking a connection.
Be careful if you implement ISession
and IAccount
interfaces on
persistent classes. You need to check if there is a transaction before
accessing the database. You may use
_context.TransactionalFactory.TransactionExists
or _context.WithTransaction
when implementing ISession.GetSession
, ISession.Validate
and
IAccount.HasAccess
methods.
public class Price{ ... [ManualTransaction] public virtual async Money Calculate(Money amount) { // no connection is used until unit converter service responds var result = await unitConverterService.Convert(amount, 'USD'); // new transaction is used to log conversion into db await _context.WithNewTransaction().DoAsync(() => { _context.New<PriceLog>().With(amount, result); }); // connection is released again return result; } ...}
Here we used the WithNewTransaction
method to obtain a new connection and a
transaction within a service. This feature is explained in detail in the
following section.
Creating New Transactions
Most of the time one transaction will be enough for business services. However there are cases when you will need a record to be updated or inserted and committed to database.
public class Company{ ... public virtual void AddEmployee(string name) { _context.WithNewTransaction().Do(() => { _context.New<Employee>().With(this, name); }); throw new Exception(); //Above insert will not be rolled back } ...}
In above example _context.WithNewTransaction().Do(() => { ... })
creates a new
database connection and begins a transaction on this new connection to provide
you with a new transaction context.
When an exception occurs in a transaction scope, Gazel catches the exception, rollbacks the transcation, closes the connection and rethrows the exception.
public class Company{ ... public virtual void AddEmployee(string name) { _context.WithNewTransaction().Do(() => { _context.New<Employee>().With(this, name); throw new Exception(); //Above insert will be rolled back }); } ...}
Using Persistent Objects
In previous example we used a variable (name
) from outer scope. This is not a
problem when variables are non-persistent objects such as string
, int
, a
manager object or any class
or record
in your modules.
Consider you need to pass a persistent object to AddEmployee
method.
public class Company{ ... public virtual void AddEmployee(string name, Branch employeeBranch) { _context.WithNewTransaction().Do(() => { //employeeBranch object will cause trouble _context.New<Employee>().With(this, name, employeeBranch); }); } ...}
When you pass a persistent object directly to a new transaction context, this
means that a Branch
instance from outer scope will be assigned to an
Employee
instance from inner scope. This causes an unexpected state for
NHibernate. To assign a persistent object to another persistent object, they
need to be loaded from the same transaction scope and ISession
instance. To
achieve this you need to pass Branch
object with a different way.
public class Company{ ... public virtual void AddEmployee(string name, Branch employeeBranch) { _context.WithNewTransaction(this, employeeBranch).Do((@this, eb) => { //correct way to pass a persistent object to new transaction _context.New<Employee>().With(@this, name, eb); }); } ...}
employeeBranch
is passed to new transaction scope using
_context.WithNewTransaction(employeeBranch).Do(eb => { ... })
. When you do
this, Gazel gets the id of given employeeBranch
, loads it in new transaction
scope and passes it as a parameter (eb
).
You can pass up to 15 parameters to WithNewTransaction
method.
Nested Transactions
You can create as many nested transactions as you want like below;
//this level uses main transaction_context.WithNewTransaction().Do(() => { //this level uses first transaction _context.WithNewTransaction().Do(() => { //this level uses second transaction _context.WithNewTransaction().Do(() => { //this level uses third transaction }); });});
When you create nested transactions, beware that you are using more than one database connections at a time.
Example: Update Balance of an Account
Below is an example to demonstrate an update to a balance in an account.
public class Account{ ... public virtual void Withdraw(Money amount) { _context.WithNewTransaction(this).Do(@this => { @this.Balance -= amount; }); } ...}
If you want to lock a record, you can make use of IRepository<T>.Lock
method.
public class Account{ ... public virtual void Withdraw(Money amount) { _context.WithNewTransaction(this).Do(@this => { //use repository of @this object. //"this._repository" uses outer scope whereas "@this._repository" uses inner scope. @this._repository.Lock(@this); @this.Balance -= amount; }); } ...}
To make it more readable, lets extract balance operation to another method.
public class Account{ ... public virtual void Withdraw(Money amount) { _context.WithNewTransaction(this).Do(@this => { @this.LockAndChangeBalance(amount); }); } private void LockAndChangeBalance(Money amount) { _repository.Lock(this); Balance -= amount } ...}
Exception Handling
Gazel uses .NET exceptions to handle errors in your business code. For every error message there should be a corresponding exception class.
Writing an Exception
Create a parent static class for each group of your exceptions and define all related exceptions under this class as a nested class.
using static MyProduct.ResultCodes;namespace MyProduct;public static class CommonExceptions{ public class NameShouldBeUnique : ServiceException { public NameShouldBeUnique() : base(Common.Err(0)) { } } public class ValueIsRequired : ServiceException { public ValueIsRequired() : base(Common.Err(1)) { } }}
Exceptions should inherit from ServiceException
to be treated as handled
errors. Handled errors are HTTP status 4XX
in REST API and they are logged
in WARN
level.
All other exceptions are treated as unhandled errors with code 99999
,
logged in ERROR
level and HTTP status is always 500
in REST API.
ResultCodes
is a class that generates error codes, but we will mention this
later in this section. You can find a detailed description in Features /
Exception Handling
Throwing an Exception
To throw an exception you can use using static
directive to include
exceptions class, MyProduct.CommonExceptions
in this case;
using static MyProduct.CommonExceptions;
And then just throw the exception like any other .NET exception;
...public class Company{ ... protected internal Company With(string name) { if(_context.Query<Companies>().AnyByName(name)) { throw new NameShouldBeUnique(); } ... } ...}...
Result Codes
Above mentioned ResultCodes
is a class where you organize your error
,
warning
and info
codes as named code blocks;
...public class ResultCodes : ResultCodeBlocks{ public static readonly ResultCodeBlock Common = CreateBlock(1, "Common");}...
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.
Every code block reserves 700
error codes, 100
warning codes and 100
info
codes. In the previous example we've seen Common.Err(0)
, this means that
NameShouldBeUnique
error should have the first error code in Common
code
block.
...public NameShouldBeUnique() : base(Common.Err(0)) { } // First error of 'Common' code block...
Here, we've started with Common.Err(0)
and can go up to Common.Err(699)
using Common
code block.
You may create a code block for every business module, so that error codes are grouped according to their business domain.
Parameterized Exceptions
You can accept parameters in your exception classes;
...public class NameShouldBeUnique : ServiceException{ public NameShouldBeUnique(string name) : base(Common.Err(0), name) { }}
Last parameter of base constructor accepts params object[] parameters
so that
you can add as many parameters as you want. This parameters are used in
building the exception message that is included in the response.
Localizing Messages
Error messages will be asked to localization with a key that is unique to each
result code. For example, NameShouldBeUnique
exception will have ERR-20701
error code.
To include parameters in messages, you can use a format string as your message
such as '{0}' already exists, name should be unique
. First parameter will
replace {0}
, second parameter will replace {1}
, and so on.