- Connection String
- Entity Layer Definitions
- Data Access Layer Definitions
- Business Layer Definitions
- Defining the Dependencies of Business and Data Access Layer
- Entity Validation Rules
- Core Layer Definitions
- Defining the Dependencies of Core Layer
- Custom Exception Middleware
- Admin Panel UI Design
When you have successfully downloaded the project, you must first change the Connection String. Connection String is defined in AsminDbContext
under Asmin.DataAccess.Concrete.EntityFramework.Context
namespace. (The project supports the Entity Framework. If you want, you can use another ORM. Independently ORM)
There are a few basic things to do when adding a new table to the database. First, a class belonging to the table must be created. This class is created under Asmin.Entities.Concrete
and inherits through BaseEntity
By the way created table must be defined in DbContext. (You know 💁)
public class TEntity : BaseEntity
{
}
A data access class is written for each database table created. The places where these definitions are made are Asmin.DataAccess.Abstract
for the interface and Asmin.DataAccess.Concrete
for concrete classes. Generic repository pattern is supported in this project. Below you can see the necessary implementations when creating a class.
Asmin.DataAccess.Abstract
public interface ITEntityDal : IRepository<TEntity>
{
}
Asmin.DataAccess.Concrete
public class TEntityDal : EfRepositoryBase<TEntity, TContext>, ITEntityDal
{
}
TContext
is normally defined as AsminDbContext
The manager classes of Entities corresponding to the database tables are written. This layer has more business codes. Validation operations, cache operations, authorization control, transaction etc. Of course these are called from a central place. The class definition of this layer is as follows.
Asmin.Business.Abstract
public interface ITEntityManager
{
}
Asmin.Business.Concrete
public class TEntityManager : ITEntityManager
{
}
When writing methods to the classes in the Business layer, we take care to comply with certain standards. In this project, the methods that send back data have the IDataResult<>
signature, while only those that perform operational operations have the IResult
signature. There are two different results for these signatures, success and error states. You can find examples below.
public IResult RemoveById(int id)
{
//some code
return new ErrorResult(ResultMessages.UserNotRemoved);
}
public async IDataResult<int> GetCount()
{
var count = // some code
return new SuccessDataResult<int>(visitorsCount);
}
As you can see above, messages are not written directly for the produced results. Instead, we receive messages from the class called ResultMessages
to provide control in one place.
In this project, transitions between layers, operational processes, service calls etc. is going to be abstracted. Of course, this is what is expected. We record the dependencies of these two layers (business and data access) through Autofac
. We define dependencies in the AutofacDependencyModule
class under Asmin.Business.DependencyModules.Autofac
namespace. For example:
namespace Asmin.Business.DependencyModules.Autofac
{
public class AutofacDependencyModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<TImplementation>().As<TInterface>();
}
}
}
After the dependencies are defined in AutofacDependencyModule
, they need to be introduced to the system by Web or API. This introductory process normally comes provided in this project, but when a new Web or API project is added in the future, you can quickly define it with this documentation.
Program.cs
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
Startup.cs
public void ConfigureContainer(ContainerBuilder builder)
{
builder.RegisterModule(new AutofacDependencyModule());
}
If you want to see other methods of adding dependencies, you can check the Autofac documentation.
We use the Fluent Validation
library when validating objects in the project. We perform the definition of the rules under Asmin.Business.ValidationRules.FluentValidation
namespace. You can find the rule definitions of a basic entity below.
public class TEntityValidator : AbstractValidator<TEntity>
{
public TEntityValidator()
{
RuleFor(entity => entity.Id).NotEmpty();
RuleFor(entity => entity.Name).NotEmpty();
}
}
After the class is written for the rules, it must be define into AutofacDependencyModule
for used with dependency injection. You can review the documentation to learn about Fluent Validation and to see other definitions.
In this layer, there are structures that will be used throughout the project. Specially the management of the structures we call Cross Cutting Concern, their writing as Aspect, services etc.
We used the benefits of Aspect Oriented Programming in this project for readability and modularity of the code. There are some Aspects defined normally in this project. These;
The Claims of the user sending the HTTP request are checked here. If the user has the desired claims, it continues. If not, the system is throwing an AuthorizationException
. Don't worry, we are catching exception. 🤫😃
An example scenario;
[AuthorizationAspect("IUserManager.AddAsync")]
public async Task<IResult> RemoveAsync(User user)
{
await _userDal.RemoveAsnyc(user);
return new SuccessResult(ResultMessages.UserRemoved);
}
If we look at the above definition, if there is no permission named IUserManager.AddAsync
in Claimsler of the user who made the HTTP request, the process will not be able to continue. Here, a method-based
process was carried out. If desired, a role-based
structure can also be implemented.
Here, we cache the return value with a special key so as not to go to the database again. The key value here is derived from the class and method name in which the method works. This will be very useful for us in the future.
An example scenario;
[CacheAspect]
public async Task<IDataResult<List<User>>> GetListAsync()
{
var users = await _userDal.GetListAsync();
return new SuccessDataResult<List<User>>(users);
}
It may not match the memory value after deleting, updating, or doing some other operational action from the database. Here, too, we delete the data that matches the key we sent to Cache with the help of Regex.
[CacheRemoveAspect("IUserManager.Get")]
public async Task<IResult> AddAsync(User user)
{
await _userDal.AddAsnyc(user);
return new SuccessResult(ResultMessages.UserAdded);
}
It is necessary to undo the operations made in the errors to be performed within the method.
An example scenario;
[AsminUnitOfWorkAspect]
public void TransactionalTestMethod()
{
User user1 = new User
{
FirstName = "Asmin",
LastName = "Yılmaz",
Email = "yusufyilmazfr@gmail.com",
Password = "123"
};
User user2 = new User
{
Email = "yusufyilmazfr@gmail.com",
Password = "123"
};
_userDal.Add(user1);
_userDal.Add(user2);
}
Above, the first user object is an accurate description, but the second object is not correct. The registration will fail, so the first registration will be undone.
It is used to catch unexpected errors in the system. For example; no database connection, system settings mismatch etc.
An example scenario;
[ExceptionAspect]
public async Task<IResult> UpdateAsync(User user)
{
await _userDal.UpdateAsnyc(user);
return new SuccessResult(ResultMessages.UserUpdated);
}
It is useful to use it in Exception Aspect, in the analysis of unexpected errors etc. it will work. Or it can be used in desired places to provide security. It is completely up to business needs. It works with Log4Net
in the background. It has the ability to record in two different locations, to the file and database. The requester's user receives the ip address, method information and parameters. This place can be changed optionally. If you want to see the details, you can go to Asmin.Core.CrossCuttingConcerns.Logging namespace
An example scenario;
[LogAspect(typeof(FileLogger))]
public async Task<IResult> AddAsync(User user)
{
await _userDal.AddAsnyc(user);
return new SuccessResult(ResultMessages.UserAdded);
}
The path information of the file where the log records written is located in the Log4Net.config
file. If you want change to path, go Log4Net.config
The Aspects described above may be insufficient according to your business needs or you may want to write something else. What you need to do here is derive the class you are creating from MethodInterception
For example:
public class CustomAspect : MethodInterception
{
}
When deriving from MethodInterceptor
, make sure using Asmin.Core.Utilities.Interceptor
is written.
After defining the class, we can operate it at any time operationally. There are processes that can be override. These; OnBefore
, OnAfter
, OnSuccess
, OnException
, Intercept
After making the definition as above, you can use it as you wish. Simple, really. 🤗
Aspects normally work from top to bottom when written. In some cases, rows play a very important role for us. In such cases, we can determine the Aspects ourselves as shown below.
[LogAspect(typeof(FileLogger), Priority = 2)]
[AuthorizationAspect("IUserManager.AddAsync", Priority = 1)]
[CacheRemoveAspect("IUserManager.Get", Priority = 3)]
public async Task<IResult> AddAsync(User user)
{
await _userDal.AddAsnyc(user);
return new SuccessResult(ResultMessages.UserAdded);
}
When we look at the code above, it works during AuthorizationAspect
> LogAspect
> CacheRemoveAspect
Microsoft Memory Cache
is default used in this project. If you want, Redis or another tool can be cached. These transactions are abstracted in the project. (This is perfect! 😍) The only thing you need to do is to implement the ICacheService
interface. These definitions are define under Asmin.Core.CrossCuttingConcerns.Caching
namespace.
MD5 hash is default defined in MD5HashService
class. However, another hash class can be defined if you want. These transactions are abstracted in the project. (You know. 🥳) The only thing you need to do is to implement the IHashService
interface. These definitions are define under Asmin.Core.Utilities.Hash
namespace.
For example:
public class CustomHashService : IHashService
{
public string CreateHash(string text)
{
//some code.
}
public bool Compare(string hashedText, string plainText)
{
//some code.
}
}
The dependencies of the core layer are defined in IServiceCollection
. We produce modules to make the dependency definitions here, then we make the definitions within the modules.
The module classes we have created implement the ICoreModule
interface. Then, in the Load (IServiceCollection services)
method, necessary dependency definitions are made.
Below is the MD5 hash service module that is defined as default in the system.
public class MD5HashModule : ICoreModule
{
public void Load(IServiceCollection services)
{
services.AddSingleton<IHashService, MD5HashService>();
}
}
We can also produce other similar modules, the same is true for Cache. If we use Redis Cache instead of the default Microsoft Memory Cache, a module is written for Redis Cache and definitions are made. In a possible change, only the module is replaced. After making the definitions in the modules, it is necessary to define these modules on the Web or API side.
What we have to do here is quite simple. To introduce the modules with the help of the extension method we wrote in the ConfigureServices
method in the Startup
class.
in Startup
class:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddDependencyModules(
new ICoreModule[]
{
new MemoryCacheModule(),
new MD5HashModule()
});
...
}
or
public void ConfigureServices(IServiceCollection services)
{
...
services.AddDependencyModules();
...
}
Since Cache and Hash services are used by Business in the above definition, their modules are introduced. If you are not going to use them in projects, you can define empty parameter, but the method definition must be made even if it is empty. The reason is that system dependencies are used in the Core Layer.
If you want to see the extension method written for IServiceCollection
, you can look at the ServiceCollectionExtensions
class under Asmin.Core.Extensions
namespace
It is not a nice behavior to give error messages directly to the user against the errors that will occur. That's why there are two middleware in the project, Web and API.
The middleware for the Web will send to the error page defined by the user in a possible error.
For example:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseMVCExceptionMiddleware("/Home/Exception");
...
}
The middleware written for the API will send the ExceptionMessage class back in case of errors. ExceptionMessage
class contains StatusCode
and Message
For example:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseAPIExceptionMiddleware();
...
}
If you want, you can look at the details in the middleware. For this you should go to Asmin.Core.Utilities.Middleware
namespace
SRTdash Admin Dashboard was used in the admin page designs. You can check the GitHub repository for documentation and other pages.
Over time, this place will be further elaborated. ⌛⌛
We are very happy that you came here, I hope it was a useful and detailed document. 🥳🎉🎉
If you like or are using this project to learn or start your solution, please give it a ⭐️. Thanks!