Abp architecture for MicroServices/SOA
Goal: Create a working sample using Abp in order to illustrate MicroServices/SOA approach.
The concept I am willing to achieve is SOA Done Right, or Microservices, or BoundedContexts if you will. They all refer to the same concept like One Service (separated from main app) should be autonomous and do not depend on anything else.
In this concept of SOA services, whithin each service you are free to use DDD, CQRS, any database you want. For now, I am using Abp/DDD approach for the Blog Module because it is the first one being developed for this sample and will adhere better with Abp for our discussions.
Sample: https://github.com/brunobertechini/abp-microservices
Items that need interaction/issues to be opened in AspnetBoilerplate:
Pending: Add the full list of requirements to this post.
I will update this list whenever I have a time-frame available.
P.s.: I have edited this HEAD post in order to have a on-going task list for this issue.
------ Original Post ---------
Hello Halil,
I would like to manage to create a complete separate module using Abp / ModuleZero in order to have the nice features like multtenancy and so on.
I can't see a way to do that with current infrastructure provided by Abp. I would like to know if you have any tips for me so I can create a fork of Abp/Abp.Zero and make the necessary changes to do that.
Related to #88 (see my comments)
Lets state the main priorities
If you just point me to the direction you have in mind I can work it out.
Bruno
Hi,
Actually, depending and sharing on a common database was harder since it requires dbcontext inheritance.
I created blog module like that since I think in a real big application, you will have a common/shared base code and plugins/modules depends on that common code.
Anyway, I think it should be easier to create such a completely independent module. I would want to create one but I'm very busy on angular2 integration nowadays. Have you tried it? What difficulties you have? Maybe I can help you on your specific problems.
Actually I've did that.
I've used the way that's used in the blog sample module but I had a tough time with database migration
@hikalkan The main problem and the first one is: Is that possible to inherit from AbpZeroDbContext without have the entities depending on main app ? Or should I go for AbpDbContext inheritance rather than AbpZeroDbContext ?
@iyhammad Can you shed some light on your DbContext? Are you inheriting from AbpDbContext and _NOT_ AbpZeroDbContext ? If so, How do you managed to use the [AutoRepositoryTypes] ? I mean, are you able to use IMyCustomModuleRepository
@iyhammad are you able to share the source code? You can ommit sensitive data of course.
Hi,
I'm inheriting from AbpDbContext. I'm not using [AutoRepositoryTypes] now. The default instance did the work for me. I can inject IRepository
I definitely share some code. Let me work on it.
@iyhammad Thanks for sharing this information.
When you say "The default instance did the work for me." what does that mean?
If you didn't specify [AutoRepositoryTypesAttribute] on your DbContext ABP will Register IRepository
So in your services you can inject IRepository
Thanks @iyhammad
I see. I had only tried inheriting from AbpZeroDbContext. I will try with AbpDbContext.
In the meantime, do you know if its possible to use multitenancy filters and softdelete without AbpZeroDbContext? Do I need to replicate the code ?
Which class register IRepository<> ? Its not working for me.
It never gets registered
Which module should I depend upon ?
You should depend on AbpZeroEntityFramework
I always recommend you to create a new empty Abp solution and have a look at it.
This particular dependency you will find it in the entity framework module in the generated solution.
@iyhammad The problem relies on remove dependencies from main app
If I derive from AbpZero the module will need the main app database in order to work propertly. I want to avoid that.
Microservices should not have dependencies (not even in runtime)
Thats the purpose of my question (this issue)
I need a way to use it without dependency on main app database (using inheritance)
The usual solution works fine (they get injected correctly) but Im trying to setup a completely independent module as per my first post.
I think if we split AbpZeroDbContext without any dbsets to a super class (abstract) we can inherit from it without getting dependencies on roles/users/tenants etc
@hikalkan is that feasible ? Would that inject correct repositories ?
I will check abpzero source code in order to see if I can find a solution for this particular case.
Anyway this is just the initial step for Abp.Soa.Microservice :)
Hi @brunobertechini
I read the whole discussion, I'm writing my response as I understand:
@hikalkan Thanks for sharing.
@hikalkan I believe you got my goal so far,
The issue Im haivng it now is: By Inheriting from AbpDbContext why IRepository and IDbContextProvider are not injected ?
I had to remove dependency on AbpZeroCoreModule because it requires classes like Tenant/User/Role (which I dont have for this specific case for example).
I already tested to create all classes pretty much like the sample blog module. But at runtime Abp does not manage to insert the correct row at the microservice database.
I made a mistake...Repositories for my dbcontext (inherited from AbpDbContext) were not being injected because i've missed to add a dependency on my data module to AbpEntityFrameworkModule.
Now they are getting injected.
Im moving forward with this approach and lets see.
I will share my findings when I have this piece working
Lets keep this discussion open :)
I'm also interested in any findings about having many independent/dynamic modules.
This is definitely the kind of architecture I'm aiming for future dev.
@hikalkan Im having this exception in this microservice trying to use AbpIntegratedTestBase
Effort.Exceptions.EffortException : Database has not been initialized
If using CodeFirst try to add the following line:
context.Database.CreateIfNotExists()
Can you please point me on the direction where the Effort Database is initialized ?
Ok as a workaround I have created the following code at AppTestBase constructor:
UsingDbContext(context =>
{
context.Database.CreateIfNotExists();
});
@hikalkan
Im trying to use a DbContext inherited from AbpDbContext but in my tests I got an exception from TenantCache which depends on IRepository
We should not add DbSet
If I add IDbSet
Abp.AbpException : There is no tenant with given id: 1
What about creating a configuration to disable tenant existence validation ?
I narrowed down to DbPerTenantConnectionStringResolver since it needs to retrieve the connection string from tenant. I couldnt find any other implementatior for SingleDb.
@brunobertechini can you share stack trace so we may see dependencies.
Effort may not be proper for such multi-db tests (at lest, current configuration in ABP template is not for that).
Also, I suggest an idea: Let build such a module together as open source. You code your simplified requirements, I review code, comment and contribute.
@hikalkan Im not using multi-db. Im using only one db (my microservice db)
I am already preparing a sample code to share using github.
It will be available soon.
Just finishing some urgent work :)
+1 i will be glad for sample code
Hello Guys,
How this story finished? did you moved to another post? there's no more comments here,
Thanks and Regards
Hi all,
I created a sample project and will want to write an article on that. Project repository: https://github.com/aspnetboilerplate/modular-todo-app
What I did:
Current main application has a direct dependency to TodoModule.Web, but it could be easily added as plugin. I added direct dependency just to make it simpler. Actually it does not depend on any class in the module.
Check source code and ask questions if you have. As I said, I'll create an article when I have time for it.
Is it possibile to do the same with angular implementation?
If you completely separate angular UI from the solution, then there is nothing ABP can do. But Angular has it's own modularity. So, you can use ABP's modularity for server side and Angular's modularity on client side in that case.
so I have to migrate to database twice while publishing, one is for ModularTodoApp, and another is TodoModule. Am I right?
It should be like that (because every module is responsible from it's own migration). You can improve it to create a tool that migrate all plugin migrations in once.
Thanks Halil, now with the new v4.1 and angular version i will come back to this thread and share my findings.
Bruno
Hello guys
I am back on track with this issue and the first thing I have to share is:
I was able to create a separated project for 02 modules still depending on basic structure from Main application.
I am now investigating and identifying each dependency so we may split up common behavior outside the main app projects in order to reuse code like Consts, FEatures, Authorization etc
I believe the first step for AspNetZero and Abp would be separate IdentityServer in its own project and Erp.Web.Host and Public and others will use that for authentication
What do you think ?
Halil, from your previous reply,
This module assumes that we are using Abp.Zero and there is AbpUsers table. It defines a new entity for the user (https://github.com/aspnetboilerplate/modular-todo-app/blob/master/src/todo-module/TodoModule.Core/Users/TodoUser.cs) which only contains properties those are needed for the TODO module. So, not depending on Abp.Zero and other unrelated entities.
How can we better approach this without dependency on AbpUsers table ? Since the module should be independent (even in another physical database server) I believe we may think about extract common things away from main app or zero.
Bruno
Hi Bruno,
I am also building an application on a very similar concepts where there will be a main application which has all the User management, configuration etc and other small applications using the same infra layer. In this case even the my need is to move identity server as a separate hosting. Each of these application will be hosted in containers
It would be very nice if you could share something, it would be of great help.
@abhishekbdutta I am finishing the first sample with this intent.
I am also thinking of separate identity server but this will make life hard when AspNetZero/Abp ships new version that needs to be updated.
I am working on that.
Steps I am working
@brunobertechini
That's a great news. waiting for the result, any time frame you can comment on.
I believe I already have a solution for this issue only (not the microservices one yet).
As this issue is : Create Abp Module without dependencies on main app, I believe I have a working solution. Still needs improvament. But I will create a sample project in github.
I only need to adapt this solution to aspnetboiplerplate because I am currently using AspNetZero and I believe I cant provide the source code in github for this sample, right @hikalkan ?
Thanks
I only need to adapt this solution to aspnetboiplerplate because I am currently using AspNetZero and I believe I cant provide the source code in github for this sample, right @hikalkan ?
@brunobertechini yes that is right since AspNet Zero is a paid product. We can also help you if you need help while adapting your solution to ABP.
Refer to issue #2467, I have a suggestion on making UI modular: modularize the Angular UI architecture is to use the multiple-application support provided by angular cli, shown in https://github.com/chanjunweimy/angular-multiple-app
I had investigated "multiple-application support provided by angular cli" before but it does not work properly. In the sample you provided it says:
Creating multiple angular cli app which each app correspond to a module
So, they are actually seperated applications.
@hikalkan Thanks for your reply! Yes, they are separated applications. However, may I know why it does not work properly? What is the ideal UI plugin solution in your opinion?
As far as I understand, plugins should be compiled separately from main application, and that is the reason why we need separated applications. For example, the ToDo module that you develop is also developed in another csproj file, which can be also viewed as "separated application".
For the multiple angular app example that I shown, I build the different applications to wwwroot and run them using aspnetcore 2.0 web server. This means that although they are developed using different angular applications, they are still hosted in the same server. I haven't investigated on the communication of different applications though. This is because I think plugins should have minimal communication and worse come to worse, I still could communicate using web api or session variables. May I ask why it does not work properly so that I could think of a way to improve it.
Thank you.
@hikalkan I have implemented the concept at here. What do you think about the implementation? Is it fine? Thanks.
I will check it when I have time. Thanks.
I've created a new project based on Abp and added some classes to the core project (settings).
Now depending on the user and field in the settings table i need to return a set of results either from my db (easy using the current setup) or from a rest service.
I'm guessing that i can use this module approach to do that? I'm not 100% sure where to put the logic to say get the data from the db or get the data from the restful service (wrapped up in another module).
Hello guys, I have created a initiative project using Abp (aspnetboilerplate) only with one custom module for now.
The backend is separated from main app. I didn't work on frontend modularization yet.
This is a start project and several things still need to be done.
I believe you guys can dive in and popup issues if needed and new tasks will arrive so we can better approach this.
The concept I am willing to achieve is SOA Done Right, or Microservices, or BoundedContexts if you will. They all refer to the same concept like One Service (separated from main app) should be autonomous and do not depend on anything else.
In this concept of SOA services, whithin each service you are free to use DDD, CQRS, any database you want. For now, I am using Abp/DDD approach for the Blog Module because it is the first one being developed for this sample and will adhere better with Abp for our discussions.
https://github.com/brunobertechini/abp-microservices
Lets keep it moving (always forward) :D
Bruno
Couple links for reference:
https://particular.net/blog/secret-of-better-ui-composition
http://udidahan.com/2016/02/19/ask-udi-two-services-operating-on-the-same-entity/
http://udidahan.com/2012/12/10/service-oriented-api-implementations/
http://udidahan.com/2012/03/05/dont-try-to-model-the-real-world-it-doesnt-exist/
http://udidahan.com/2014/05/26/people-politics-and-the-single-responsibility-principle/
@chanjunweimy i would add to your comment that my initial goal is not create plugins. But rather independent services that communicate to each other using async patterns. This apply to backend and frontend. The service must be vertical (own its data from top html down to database).
Thats the definition of "autonomous service". My focus is on large applications using SOA Principles.
On the angular side for example, there will be "client side contracts" between each service's ui components. This all based on events (thanks to angular for that).
This way, we only need to deploy side by side and use the BackendService or ITOps service (so -called). You can read more about this in the links I have provided in previous comment.
@neilgibert1234
I've created a new project based on Abp and added some classes to the core project (settings).
Now depending on the user and field in the settings table i need to return a set of results either from my db (easy using the current setup) or from a rest service.
I'm guessing that i can use this module approach to do that? I'm not 100% sure where to put the logic to say get the data from the db or get the data from the restful service (wrapped up in another module).
Please keep in mind that even the "settings" for your service (the flag stating where to retrieve data from) belongs to the service itself. It should be self contained. In Abp terms, you can use the SettingsProvider for your custom service and elaborate an UI to allow user input.
Then in your AppService from your custom service you are free to "query" this setting (never outside the service itself neither from other modules/services).
That way you can use Pure OO like Factory Pattern to retrieve the instance.
Please let me know if you have any doubts and take a look at my sample. If you want, we could build another module in this sample just for your case. After all, this is the purpose of this abp-microservices initiative
How would I specify where the To Do Navigation Menu Item is placed. It appears that now it will be appended to the abp.zero navigation..
Example:
Get a menu item and add your new menu item as child of the menu item. Example:
C#
public override void SetNavigation(INavigationProviderContext context)
{
var adminMenu = context.Manager.MainMenu.GetItemByName("Abp.Zero.Administration");
adminMenu.AddItem(
new MenuItemDefinition(
"Abp.Zero.Administration.Setting",
new FixedLocalizableString("Setting management"),
icon: "fa fa-cog",
url: "#/admin/settings",
customData: new MyCustomDataClass { Data1 = 42, Data2 = "FortyTwo" }
)
);
}
In addition (not related to your case) https://github.com/aspnetboilerplate/aspnetboilerplate/blob/dev/src/Abp/Application/Navigation/MenuItemDefinitionExtensions.cs defines good methods to re-locate menu items.
Hi @brunobertechini, is there any update on your part regarding the Micro services project.
Any time line for it so as to run the micro service as a different application/host
Hi @abhishekbdutta
Hi @brunobertechini, is there any update on your part regarding the Micro services project.
Im working on the servicebus side as this is the main line feature for our needs at this moment.
Any time line for it so as to run the micro service as a different application/host
Can you please elaborate? Would you like to run the separated module as a another process like in another aspnetcore deployment and/or docker container for example ?
If so, this is a bit tricky in current Abp implementation. In my point of view this will require:
I will try to create another service sample. I will keep BlogService running side by side in the Microservices.Web.Host abp endpoint and create a new service with this approach in mind.
@abhishekbdutta do you have available time to cooperate?
Hi @brunobertechini , I am trying to get a better understanding of your project and architecture, and I have learnt a lot just by reading the code. I am still very new to SOA and microservices, may I know how are migrations executed automatically for each service?
Hello @Juslwk , sorry for the delay. They are included in each module's EntityFramework Module Initialize (or post initialize).
Every module use Migrations Api to update its own database.
@chanjunweimy Would you like to create a shared repo to work integratingmy backend structure with your UI structure?
I believe we are in the right path
Bruno
@brunobertechini yes, we can try that. ^^
Jun Wei
@hikalkan
I'll create an article when I have time for it.
Any Update please
Not yet, sorry.
Hi, @hikalkan. I studied your modular todo example at https://github.com/aspnetboilerplate/modular-todo-app. I'm thinking about the same way to do a modular application. However, I did not understand how you deal with db migration. I found that in your module's migration.cs file(https://github.com/aspnetboilerplate/modular-todo-app/blob/master/src/todo-module/TodoModule.EntityFramework/Migrations/201702031754143_Initial_Todo_Migration.cs), there is no creating table for TodoUser. I'm wondering how to generate migration code for a module so that do not create table for entities like TodoUser in your example?
@eureky because TodoUser is mapped to same table with AbpUser, see https://github.com/aspnetboilerplate/modular-todo-app/blob/master/src/todo-module/TodoModule.Core/Users/TodoUser.cs#L6. If you map an entity to a new table in your module, it should be created in the migrations.
@ismcagdas thanks. I'm aware of the table mapping. I've tried to create a User entity in the first project and created Users table in db, then a MyUser entity in the second project which is mapped to the same Users table in the same db, but add-migration tool generated xxxxxxx_migration.cs file still contains code for creating Users table. Is there a way to prevent add-migration from creating the existing tables ? or do I have to to modify the generated migration code by hand ?
I've tried to create a User entity in the first project and created Users table in db
Have you created the table by migration at the first place ?
Yes, I have run update-database in project 1 first. and Users table is already exist in database. If try to run update-database in project 2, it will fail with error like "There is already an object named 'Users' in the database." @ismcagdas
@ismcagdas When we have multiple contexts with depending on AbpDbContext at migration time Abp generating basic tables (AbpUser, Settings, AuditedLog,...). I need to remove all duplicate model builder from myNewMigration.cs
My module not have any users navigation else one entity with FullAudited interface. So ABP generated their related tables. I don't know why its happen when their uses same DB & same connectionString.
@eureky You can remove duplicate tables from yourMigration.cs in your customModule to running your Db-Update without this error. Then waiting for fix issue.
@eureky I think I understand your problem now but this is related to EF rather than ABP.
If you have two different db context and want to use same table for different entities in those different dbContexts, you need to implement a custom solution because EF cannot now if there is a table or not for other dbContexts.
So, you can create a 3. DbContext just for managing migrations. Then you can create two interfaces for your DbContests. IDbContext1, IDbContext2 for example and define your DbSets in those interfaces.
Derive your 3. DbContext from both IDbContext1, IDbContext2 and use it for the migrations.
Probably there will be some problematic cases like when you want to use same entity in both dbContexts. In that case, you can override OnModelCreating and ignore them manually.
@ismcagdas Yes, I understand it's EF. So can I infer that in the Todo example, the code for creating AbpUser table in todo module was removed manually ? (https://github.com/aspnetboilerplate/modular-todo-app/blob/master/src/todo-module/TodoModule.EntityFramework/Migrations/201702031754143_Initial_Todo_Migration.cs)
I will look into and try your solution later. I think it's OK for a module to manage migrations for it's own entities. But the major problem is the entity referenced from other module. Looks like it can only be handled in a manual way.
@eureky migrations are disabled for Todo Module's DbContext https://github.com/aspnetboilerplate/modular-todo-app/blob/master/src/todo-module/TodoModule.EntityFramework/Migrations/Configuration.cs and TodoUser doesn't add a new field to existing User entity (AbpUsers table).
@eureky , @ismcagdas , @hitaspdotnet
Just read your doubts/comments and here are my two cents:
First of all, I believe we are talking about AbpZeroDbContext and not AbpDbContext correct?
This Abp problem of AbpZeroDbContext is one (very important) issue that drived me to create this project to accomplish complete separation of concerns.
If you inherit from AbpZeroDbContext you are saying your module MUST HAVE all tables implemented by it (users, roles, etc -- provided by module zero). This should be a concern only for your "platform" or main app module. Not for every custom module.
Even if you REMOVE the instructions from migration file, the RUNTIME dependency still remains (at runtime it will search and look for tables).
This is a big controversial issue.
My recommendation for now, is dont inherit from AbpZeroDbContext, instead, use AbpDbContext. for a clean environment and add your dbsets.
Please let me know if you have any doubts.
Bruno
Updated first post with more TODO items.
When we have multiple contexts with depending on AbpDbContext at migration time Abp generating basic tables (AbpUser, Settings, AuditedLog,...).
I referred directly to AbpDbContext. Where did you see AbpZeroDbContext in my comment?
In fact, you cannot use AbpZeroDbContext without referring to the user, roles and tenant entity.
@hitaspdotnet AbpDbContext does not contain USers and Roles. So there is no way migrations creating instructions for these tables:
@ismcagdas When we have multiple contexts with depending on AbpDbContext at migration time Abp generating basic tables (AbpUser, Settings, AuditedLog,...). I need to remove all duplicate model builder from myNewMigration.cs
My module not have any users navigation else one entity with FullAudited interface. So ABP generated their related tables. I don't know why its happen when their uses some DB & same connectionString.
AbpDbContext does not generate that.
Bruno
@hitaspdotnet can you share some code?
public partial class Initial_Migration : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AbpLanguages",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
CreationTime = table.Column<DateTime>(nullable: false),
CreatorUserId = table.Column<long>(nullable: true),
DeleterUserId = table.Column<long>(nullable: true),
DeletionTime = table.Column<DateTime>(nullable: true),
DisplayName = table.Column<string>(maxLength: 64, nullable: false),
Icon = table.Column<string>(maxLength: 128, nullable: true),
IsDeleted = table.Column<bool>(nullable: false),
IsDisabled = table.Column<bool>(nullable: false),
LastModificationTime = table.Column<DateTime>(nullable: true),
LastModifierUserId = table.Column<long>(nullable: true),
Name = table.Column<string>(maxLength: 10, nullable: false),
TenantId = table.Column<int>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AbpLanguages", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AbpUsers",
columns: table => new
{
Id = table.Column<long>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
AccessFailedCount = table.Column<int>(nullable: false),
AuthenticationSource = table.Column<string>(maxLength: 64, nullable: true),
ConcurrencyStamp = table.Column<string>(maxLength: 128, nullable: true),
CreationTime = table.Column<DateTime>(nullable: false),
CreatorUserId = table.Column<long>(nullable: true),
DefaultBillingAddressId = table.Column<long>(nullable: true),
DefaultShippingAddressId = table.Column<long>(nullable: true),
DeleterUserId = table.Column<long>(nullable: true),
DeletionTime = table.Column<DateTime>(nullable: true),
EmailAddress = table.Column<string>(maxLength: 256, nullable: false),
EmailConfirmationCode = table.Column<string>(maxLength: 328, nullable: true),
GoogleAuthenticatorKey = table.Column<string>(nullable: true),
HomeLocationId = table.Column<long>(nullable: true),
InstagramLink = table.Column<string>(nullable: true),
IsActive = table.Column<bool>(nullable: false),
IsDeleted = table.Column<bool>(nullable: false),
IsEmailConfirmed = table.Column<bool>(nullable: false),
IsLockoutEnabled = table.Column<bool>(nullable: false),
IsPhoneNumberConfirmed = table.Column<bool>(nullable: false),
IsTwoFactorEnabled = table.Column<bool>(nullable: false),
LastLoginTime = table.Column<DateTime>(nullable: true),
LastModificationTime = table.Column<DateTime>(nullable: true),
LastModifierUserId = table.Column<long>(nullable: true),
LockoutEndDateUtc = table.Column<DateTime>(nullable: true),
Name = table.Column<string>(maxLength: 32, nullable: false),
NormalizedEmailAddress = table.Column<string>(maxLength: 256, nullable: false),
NormalizedUserName = table.Column<string>(maxLength: 32, nullable: false),
Password = table.Column<string>(maxLength: 128, nullable: false),
PasswordResetCode = table.Column<string>(maxLength: 328, nullable: true),
PhoneNumber = table.Column<string>(maxLength: 32, nullable: true),
ProfilePictureId = table.Column<Guid>(nullable: true),
SecurityStamp = table.Column<string>(maxLength: 128, nullable: true),
ShareSocials = table.Column<bool>(nullable: false),
ShouldChangePasswordOnNextLogin = table.Column<bool>(nullable: false),
SignInToken = table.Column<string>(nullable: true),
SignInTokenExpireTimeUtc = table.Column<DateTime>(nullable: true),
Surname = table.Column<string>(maxLength: 32, nullable: false),
TelegramLink = table.Column<string>(nullable: true),
TenantId = table.Column<int>(nullable: true),
TwitterLink = table.Column<string>(nullable: true),
UserGuid = table.Column<Guid>(nullable: false),
UserName = table.Column<string>(maxLength: 32, nullable: false),
VendorId = table.Column<long>(nullable: true),
WorkLocationId = table.Column<long>(nullable: true),
YouTubeLink = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AbpUsers", x => x.Id);
});
migrationBuilder.CreateTable(
name: "BlogCategories",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
CreationTime = table.Column<DateTime>(nullable: false),
CreatorUserId = table.Column<long>(nullable: true),
DeleterUserId = table.Column<long>(nullable: true),
DeletionTime = table.Column<DateTime>(nullable: true),
Description = table.Column<string>(maxLength: 2000, nullable: true),
IsDeleted = table.Column<bool>(nullable: false),
LastModificationTime = table.Column<DateTime>(nullable: true),
LastModifierUserId = table.Column<long>(nullable: true),
Name = table.Column<string>(maxLength: 128, nullable: false),
ShortDescription = table.Column<string>(maxLength: 400, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_BlogCategories", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AbpPermissions",
columns: table => new
{
Id = table.Column<long>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
CreationTime = table.Column<DateTime>(nullable: false),
CreatorUserId = table.Column<long>(nullable: true),
IsGranted = table.Column<bool>(nullable: false),
Name = table.Column<string>(maxLength: 128, nullable: false),
TenantId = table.Column<int>(nullable: true),
UserId = table.Column<long>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AbpPermissions", x => x.Id);
table.ForeignKey(
name: "FK_AbpPermissions_AbpUsers_UserId",
column: x => x.UserId,
principalTable: "AbpUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AbpSettings",
columns: table => new
{
Id = table.Column<long>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
CreationTime = table.Column<DateTime>(nullable: false),
CreatorUserId = table.Column<long>(nullable: true),
LastModificationTime = table.Column<DateTime>(nullable: true),
LastModifierUserId = table.Column<long>(nullable: true),
Name = table.Column<string>(maxLength: 256, nullable: false),
TenantId = table.Column<int>(nullable: true),
UserId = table.Column<long>(nullable: true),
Value = table.Column<string>(maxLength: 2000, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AbpSettings", x => x.Id);
table.ForeignKey(
name: "FK_AbpSettings_AbpUsers_UserId",
column: x => x.UserId,
principalTable: "AbpUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "AbpUserClaims",
columns: table => new
{
Id = table.Column<long>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
ClaimType = table.Column<string>(maxLength: 256, nullable: true),
ClaimValue = table.Column<string>(nullable: true),
CreationTime = table.Column<DateTime>(nullable: false),
CreatorUserId = table.Column<long>(nullable: true),
TenantId = table.Column<int>(nullable: true),
UserId = table.Column<long>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AbpUserClaims", x => x.Id);
table.ForeignKey(
name: "FK_AbpUserClaims_AbpUsers_UserId",
column: x => x.UserId,
principalTable: "AbpUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AbpUserLogins",
columns: table => new
{
Id = table.Column<long>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
LoginProvider = table.Column<string>(maxLength: 128, nullable: false),
ProviderKey = table.Column<string>(maxLength: 256, nullable: false),
TenantId = table.Column<int>(nullable: true),
UserId = table.Column<long>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AbpUserLogins", x => x.Id);
table.ForeignKey(
name: "FK_AbpUserLogins_AbpUsers_UserId",
column: x => x.UserId,
principalTable: "AbpUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AbpUserRoles",
columns: table => new
{
Id = table.Column<long>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
CreationTime = table.Column<DateTime>(nullable: false),
CreatorUserId = table.Column<long>(nullable: true),
RoleId = table.Column<int>(nullable: false),
TenantId = table.Column<int>(nullable: true),
UserId = table.Column<long>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AbpUserRoles", x => x.Id);
table.ForeignKey(
name: "FK_AbpUserRoles_AbpUsers_UserId",
column: x => x.UserId,
principalTable: "AbpUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AbpUserTokens",
columns: table => new
{
Id = table.Column<long>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
LoginProvider = table.Column<string>(maxLength: 64, nullable: true),
Name = table.Column<string>(maxLength: 128, nullable: true),
TenantId = table.Column<int>(nullable: true),
UserId = table.Column<long>(nullable: false),
Value = table.Column<string>(maxLength: 512, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AbpUserTokens", x => x.Id);
table.ForeignKey(
name: "FK_AbpUserTokens_AbpUsers_UserId",
column: x => x.UserId,
principalTable: "AbpUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "BlogPosts",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
AllowComments = table.Column<bool>(nullable: false),
ApplicationLanguageId = table.Column<int>(nullable: false),
BlogCategoryId = table.Column<int>(nullable: false),
Body = table.Column<string>(nullable: false),
BodyOverview = table.Column<string>(nullable: true),
CreationTime = table.Column<DateTime>(nullable: false),
CreatorUserId = table.Column<long>(nullable: true),
EndDateUtc = table.Column<DateTime>(nullable: true),
MetaDescription = table.Column<string>(nullable: true),
MetaKeywords = table.Column<string>(maxLength: 400, nullable: true),
MetaTitle = table.Column<string>(maxLength: 400, nullable: true),
StartDateUtc = table.Column<DateTime>(nullable: true),
Status = table.Column<int>(nullable: false),
Tags = table.Column<string>(nullable: true),
TenantId = table.Column<int>(nullable: true),
Title = table.Column<string>(maxLength: 400, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_BlogPosts", x => x.Id);
table.ForeignKey(
name: "FK_BlogPosts_AbpLanguages_ApplicationLanguageId",
column: x => x.ApplicationLanguageId,
principalTable: "AbpLanguages",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_BlogPosts_BlogCategories_BlogCategoryId",
column: x => x.BlogCategoryId,
principalTable: "BlogCategories",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "BlogComment",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
BlogPostId = table.Column<int>(nullable: false),
CommentText = table.Column<string>(nullable: false),
CreationTime = table.Column<DateTime>(nullable: false),
CreatorUserId = table.Column<long>(nullable: true),
IsApproved = table.Column<bool>(nullable: false),
TenantId = table.Column<int>(nullable: true),
UserId = table.Column<long>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_BlogComment", x => x.Id);
table.ForeignKey(
name: "FK_BlogComment_BlogPosts_BlogPostId",
column: x => x.BlogPostId,
principalTable: "BlogPosts",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_BlogComment_AbpUsers_UserId",
column: x => x.UserId,
principalTable: "AbpUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AbpPermissions_UserId",
table: "AbpPermissions",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AbpSettings_UserId",
table: "AbpSettings",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AbpUserClaims_UserId",
table: "AbpUserClaims",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AbpUserLogins_UserId",
table: "AbpUserLogins",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AbpUserRoles_UserId",
table: "AbpUserRoles",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AbpUserTokens_UserId",
table: "AbpUserTokens",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_BlogComment_BlogPostId",
table: "BlogComment",
column: "BlogPostId");
migrationBuilder.CreateIndex(
name: "IX_BlogComment_UserId",
table: "BlogComment",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_BlogPosts_ApplicationLanguageId",
table: "BlogPosts",
column: "ApplicationLanguageId");
migrationBuilder.CreateIndex(
name: "IX_BlogPosts_BlogCategoryId",
table: "BlogPosts",
column: "BlogCategoryId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AbpPermissions");
migrationBuilder.DropTable(
name: "AbpSettings");
migrationBuilder.DropTable(
name: "AbpUserClaims");
migrationBuilder.DropTable(
name: "AbpUserLogins");
migrationBuilder.DropTable(
name: "AbpUserRoles");
migrationBuilder.DropTable(
name: "AbpUserTokens");
migrationBuilder.DropTable(
name: "BlogComment");
migrationBuilder.DropTable(
name: "BlogPosts");
migrationBuilder.DropTable(
name: "AbpUsers");
migrationBuilder.DropTable(
name: "AbpLanguages");
migrationBuilder.DropTable(
name: "BlogCategories");
}
}
And DbContext
[AutoRepositoryTypes(
typeof(IRepository<>),
typeof(IRepository<,>),
typeof(BlogServiceRepositoryBase<>),
typeof(BlogServiceRepositoryBase<,>)
)]
public class BlogServiceDbContext : AbpDbContext
{
public virtual DbSet<BlogCategory> BlogCategories { get; set; }
public virtual DbSet<BlogComment> BlogComments { get; set; }
public virtual DbSet<BlogPost> BlogPosts { get; set; }
public BlogServiceDbContext(DbContextOptions<BlogServiceDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
ConfigureEntities(modelBuilder);
}
private void ConfigureEntities(ModelBuilder modelBuilder)
{
}
}
BlogPost and BlogComment entities depended on FullAuditedEntity
Even if you REMOVE the instructions from migration file, the RUNTIME dependency still remains (at runtime it will search and look for tables).
This is a big controversial issue.
I have ready to run multi-language blog service with all available Abp wonderful feature.
But I have big problem with adding BlogService localize source to source manager for editing values from UI. https://github.com/aspnetzero/aspnet-zero-core/issues/1074#issuecomment-385975138
@hitaspdotnet Regarding your BlogServiceDbContext , is this the only dbcontext in your whole solution ?
Is this a separated service? This code should not generate more than 3 tables specified as your DbSets.
Please check if this dbcontext is not being added twice alltogether with default main app dbcontext.
Are you able to share a repository so I can help you out ?
Perhaps this is something related to configuration. I remember I had this issue in the past, but I need to recall from my git history how I did it.
If you look at the source code of 3.5.0 AbpDbContext.cs, you can see that there is no DbSet configured, thus, no tables should be generated.
```c#
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Abp.Collections.Extensions;
using Abp.Configuration.Startup;
using Abp.Dependency;
using Abp.Domain.Entities;
using Abp.Domain.Entities.Auditing;
using Abp.Domain.Uow;
using Abp.Events.Bus;
using Abp.Events.Bus.Entities;
using Abp.Extensions;
using Abp.Reflection;
using Abp.Runtime.Session;
using Abp.Timing;
using Castle.Core.Logging;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Metadata;
namespace Abp.EntityFrameworkCore
{
///
/// Base class for all DbContext classes in the application.
///
public abstract class AbpDbContext : DbContext, ITransientDependency
{
///
/// Used to get current session values.
///
public IAbpSession AbpSession { get; set; }
/// <summary>
/// Used to trigger entity change events.
/// </summary>
public IEntityChangeEventHelper EntityChangeEventHelper { get; set; }
/// <summary>
/// Reference to the logger.
/// </summary>
public ILogger Logger { get; set; }
/// <summary>
/// Reference to the event bus.
/// </summary>
public IEventBus EventBus { get; set; }
/// <summary>
/// Reference to GUID generator.
/// </summary>
public IGuidGenerator GuidGenerator { get; set; }
/// <summary>
/// Reference to the current UOW provider.
/// </summary>
public ICurrentUnitOfWorkProvider CurrentUnitOfWorkProvider { get; set; }
/// <summary>
/// Reference to multi tenancy configuration.
/// </summary>
public IMultiTenancyConfig MultiTenancyConfig { get; set; }
/// <summary>
/// Can be used to suppress automatically setting TenantId on SaveChanges.
/// Default: false.
/// </summary>
public virtual bool SuppressAutoSetTenantId { get; set; }
protected virtual int? CurrentTenantId => GetCurrentTenantIdOrNull();
protected virtual bool IsSoftDeleteFilterEnabled => CurrentUnitOfWorkProvider?.Current?.IsFilterEnabled(AbpDataFilters.SoftDelete) == true;
protected virtual bool IsMayHaveTenantFilterEnabled => CurrentUnitOfWorkProvider?.Current?.IsFilterEnabled(AbpDataFilters.MayHaveTenant) == true;
protected virtual bool IsMustHaveTenantFilterEnabled => CurrentTenantId != null && CurrentUnitOfWorkProvider?.Current?.IsFilterEnabled(AbpDataFilters.MustHaveTenant) == true;
private static MethodInfo ConfigureGlobalFiltersMethodInfo = typeof(AbpDbContext).GetMethod(nameof(ConfigureGlobalFilters), BindingFlags.Instance | BindingFlags.NonPublic);
/// <summary>
/// Constructor.
/// </summary>
protected AbpDbContext(DbContextOptions options)
: base(options)
{
InitializeDbContext();
}
private void InitializeDbContext()
{
SetNullsForInjectedProperties();
}
private void SetNullsForInjectedProperties()
{
Logger = NullLogger.Instance;
AbpSession = NullAbpSession.Instance;
EntityChangeEventHelper = NullEntityChangeEventHelper.Instance;
GuidGenerator = SequentialGuidGenerator.Instance;
EventBus = NullEventBus.Instance;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
ConfigureGlobalFiltersMethodInfo
.MakeGenericMethod(entityType.ClrType)
.Invoke(this, new object[] { modelBuilder, entityType });
}
}
protected void ConfigureGlobalFilters<TEntity>(ModelBuilder modelBuilder, IMutableEntityType entityType)
where TEntity : class
{
if (entityType.BaseType == null && ShouldFilterEntity<TEntity>(entityType))
{
var filterExpression = CreateFilterExpression<TEntity>();
if (filterExpression != null)
{
modelBuilder.Entity<TEntity>().HasQueryFilter(filterExpression);
}
}
}
protected virtual bool ShouldFilterEntity<TEntity>(IMutableEntityType entityType) where TEntity : class
{
if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)))
{
return true;
}
if (typeof(IMayHaveTenant).IsAssignableFrom(typeof(TEntity)))
{
return true;
}
if (typeof(IMustHaveTenant).IsAssignableFrom(typeof(TEntity)))
{
return true;
}
return false;
}
protected virtual Expression<Func<TEntity, bool>> CreateFilterExpression<TEntity>()
where TEntity : class
{
Expression<Func<TEntity, bool>> expression = null;
if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)))
{
/* This condition should normally be defined as below:
* !IsSoftDeleteFilterEnabled || !((ISoftDelete) e).IsDeleted
* But this causes a problem with EF Core (see https://github.com/aspnet/EntityFrameworkCore/issues/9502)
* So, we made a workaround to make it working. It works same as above.
*/
Expression<Func<TEntity, bool>> softDeleteFilter = e => !((ISoftDelete)e).IsDeleted || ((ISoftDelete)e).IsDeleted != IsSoftDeleteFilterEnabled;
expression = expression == null ? softDeleteFilter : CombineExpressions(expression, softDeleteFilter);
}
if (typeof(IMayHaveTenant).IsAssignableFrom(typeof(TEntity)))
{
/* This condition should normally be defined as below:
* !IsMayHaveTenantFilterEnabled || ((IMayHaveTenant)e).TenantId == CurrentTenantId
* But this causes a problem with EF Core (see https://github.com/aspnet/EntityFrameworkCore/issues/9502)
* So, we made a workaround to make it working. It works same as above.
*/
Expression<Func<TEntity, bool>> mayHaveTenantFilter = e => ((IMayHaveTenant)e).TenantId == CurrentTenantId || (((IMayHaveTenant)e).TenantId == CurrentTenantId) == IsMayHaveTenantFilterEnabled;
expression = expression == null ? mayHaveTenantFilter : CombineExpressions(expression, mayHaveTenantFilter);
}
if (typeof(IMustHaveTenant).IsAssignableFrom(typeof(TEntity)))
{
/* This condition should normally be defined as below:
* !IsMustHaveTenantFilterEnabled || ((IMustHaveTenant)e).TenantId == CurrentTenantId
* But this causes a problem with EF Core (see https://github.com/aspnet/EntityFrameworkCore/issues/9502)
* So, we made a workaround to make it working. It works same as above.
*/
Expression<Func<TEntity, bool>> mustHaveTenantFilter = e => ((IMustHaveTenant)e).TenantId == CurrentTenantId || (((IMustHaveTenant)e).TenantId == CurrentTenantId) == IsMustHaveTenantFilterEnabled;
expression = expression == null ? mustHaveTenantFilter : CombineExpressions(expression, mustHaveTenantFilter);
}
return expression;
}
public override int SaveChanges()
{
try
{
var changeReport = ApplyAbpConcepts();
var result = base.SaveChanges();
EntityChangeEventHelper.TriggerEvents(changeReport);
return result;
}
catch (DbUpdateConcurrencyException ex)
{
throw new AbpDbConcurrencyException(ex.Message, ex);
}
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
{
try
{
var changeReport = ApplyAbpConcepts();
var result = await base.SaveChangesAsync(cancellationToken);
await EntityChangeEventHelper.TriggerEventsAsync(changeReport);
return result;
}
catch (DbUpdateConcurrencyException ex)
{
throw new AbpDbConcurrencyException(ex.Message, ex);
}
}
protected virtual EntityChangeReport ApplyAbpConcepts()
{
var changeReport = new EntityChangeReport();
var userId = GetAuditUserId();
foreach (var entry in ChangeTracker.Entries().ToList())
{
ApplyAbpConcepts(entry, userId, changeReport);
}
return changeReport;
}
protected virtual void ApplyAbpConcepts(EntityEntry entry, long? userId, EntityChangeReport changeReport)
{
switch (entry.State)
{
case EntityState.Added:
ApplyAbpConceptsForAddedEntity(entry, userId, changeReport);
break;
case EntityState.Modified:
ApplyAbpConceptsForModifiedEntity(entry, userId, changeReport);
break;
case EntityState.Deleted:
ApplyAbpConceptsForDeletedEntity(entry, userId, changeReport);
break;
}
AddDomainEvents(changeReport.DomainEvents, entry.Entity);
}
protected virtual void ApplyAbpConceptsForAddedEntity(EntityEntry entry, long? userId, EntityChangeReport changeReport)
{
CheckAndSetId(entry);
CheckAndSetMustHaveTenantIdProperty(entry.Entity);
CheckAndSetMayHaveTenantIdProperty(entry.Entity);
SetCreationAuditProperties(entry.Entity, userId);
changeReport.ChangedEntities.Add(new EntityChangeEntry(entry.Entity, EntityChangeType.Created));
}
protected virtual void ApplyAbpConceptsForModifiedEntity(EntityEntry entry, long? userId, EntityChangeReport changeReport)
{
SetModificationAuditProperties(entry.Entity, userId);
if (entry.Entity is ISoftDelete && entry.Entity.As<ISoftDelete>().IsDeleted)
{
SetDeletionAuditProperties(entry.Entity, userId);
changeReport.ChangedEntities.Add(new EntityChangeEntry(entry.Entity, EntityChangeType.Deleted));
}
else
{
changeReport.ChangedEntities.Add(new EntityChangeEntry(entry.Entity, EntityChangeType.Updated));
}
}
protected virtual void ApplyAbpConceptsForDeletedEntity(EntityEntry entry, long? userId, EntityChangeReport changeReport)
{
CancelDeletionForSoftDelete(entry);
SetDeletionAuditProperties(entry.Entity, userId);
changeReport.ChangedEntities.Add(new EntityChangeEntry(entry.Entity, EntityChangeType.Deleted));
}
protected virtual void AddDomainEvents(List<DomainEventEntry> domainEvents, object entityAsObj)
{
var generatesDomainEventsEntity = entityAsObj as IGeneratesDomainEvents;
if (generatesDomainEventsEntity == null)
{
return;
}
if (generatesDomainEventsEntity.DomainEvents.IsNullOrEmpty())
{
return;
}
domainEvents.AddRange(generatesDomainEventsEntity.DomainEvents.Select(eventData => new DomainEventEntry(entityAsObj, eventData)));
generatesDomainEventsEntity.DomainEvents.Clear();
}
protected virtual void CheckAndSetId(EntityEntry entry)
{
//Set GUID Ids
var entity = entry.Entity as IEntity<Guid>;
if (entity != null && entity.Id == Guid.Empty)
{
var dbGeneratedAttr = ReflectionHelper
.GetSingleAttributeOrDefault<DatabaseGeneratedAttribute>(
entry.Property("Id").Metadata.PropertyInfo
);
if (dbGeneratedAttr == null || dbGeneratedAttr.DatabaseGeneratedOption == DatabaseGeneratedOption.None)
{
entity.Id = GuidGenerator.Create();
}
}
}
protected virtual void CheckAndSetMustHaveTenantIdProperty(object entityAsObj)
{
if (SuppressAutoSetTenantId)
{
return;
}
//Only set IMustHaveTenant entities
if (!(entityAsObj is IMustHaveTenant))
{
return;
}
var entity = entityAsObj.As<IMustHaveTenant>();
//Don't set if it's already set
if (entity.TenantId != 0)
{
return;
}
var currentTenantId = GetCurrentTenantIdOrNull();
if (currentTenantId != null)
{
entity.TenantId = currentTenantId.Value;
}
else
{
throw new AbpException("Can not set TenantId to 0 for IMustHaveTenant entities!");
}
}
protected virtual void CheckAndSetMayHaveTenantIdProperty(object entityAsObj)
{
if (SuppressAutoSetTenantId)
{
return;
}
//Only works for single tenant applications
if (MultiTenancyConfig?.IsEnabled ?? false)
{
return;
}
//Only set IMayHaveTenant entities
if (!(entityAsObj is IMayHaveTenant))
{
return;
}
var entity = entityAsObj.As<IMayHaveTenant>();
//Don't set if it's already set
if (entity.TenantId != null)
{
return;
}
entity.TenantId = GetCurrentTenantIdOrNull();
}
protected virtual void SetCreationAuditProperties(object entityAsObj, long? userId)
{
EntityAuditingHelper.SetCreationAuditProperties(MultiTenancyConfig, entityAsObj, AbpSession.TenantId, userId);
}
protected virtual void SetModificationAuditProperties(object entityAsObj, long? userId)
{
EntityAuditingHelper.SetModificationAuditProperties(MultiTenancyConfig, entityAsObj, AbpSession.TenantId, userId);
}
protected virtual void CancelDeletionForSoftDelete(EntityEntry entry)
{
if (!(entry.Entity is ISoftDelete))
{
return;
}
entry.Reload();
entry.State = EntityState.Modified;
entry.Entity.As<ISoftDelete>().IsDeleted = true;
}
protected virtual void SetDeletionAuditProperties(object entityAsObj, long? userId)
{
if (entityAsObj is IHasDeletionTime)
{
var entity = entityAsObj.As<IHasDeletionTime>();
if (entity.DeletionTime == null)
{
entity.DeletionTime = Clock.Now;
}
}
if (entityAsObj is IDeletionAudited)
{
var entity = entityAsObj.As<IDeletionAudited>();
if (entity.DeleterUserId != null)
{
return;
}
if (userId == null)
{
entity.DeleterUserId = null;
return;
}
//Special check for multi-tenant entities
if (entity is IMayHaveTenant || entity is IMustHaveTenant)
{
//Sets LastModifierUserId only if current user is in same tenant/host with the given entity
if ((entity is IMayHaveTenant && entity.As<IMayHaveTenant>().TenantId == AbpSession.TenantId) ||
(entity is IMustHaveTenant && entity.As<IMustHaveTenant>().TenantId == AbpSession.TenantId))
{
entity.DeleterUserId = userId;
}
else
{
entity.DeleterUserId = null;
}
}
else
{
entity.DeleterUserId = userId;
}
}
}
protected virtual long? GetAuditUserId()
{
if (AbpSession.UserId.HasValue &&
CurrentUnitOfWorkProvider != null &&
CurrentUnitOfWorkProvider.Current != null &&
CurrentUnitOfWorkProvider.Current.GetTenantId() == AbpSession.TenantId)
{
return AbpSession.UserId;
}
return null;
}
protected virtual int? GetCurrentTenantIdOrNull()
{
if (CurrentUnitOfWorkProvider != null &&
CurrentUnitOfWorkProvider.Current != null)
{
return CurrentUnitOfWorkProvider.Current.GetTenantId();
}
return AbpSession.TenantId;
}
protected virtual Expression<Func<T, bool>> CombineExpressions<T>(Expression<Func<T, bool>> expression1, Expression<Func<T, bool>> expression2)
{
var parameter = Expression.Parameter(typeof(T));
var leftVisitor = new ReplaceExpressionVisitor(expression1.Parameters[0], parameter);
var left = leftVisitor.Visit(expression1.Body);
var rightVisitor = new ReplaceExpressionVisitor(expression2.Parameters[0], parameter);
var right = rightVisitor.Visit(expression2.Body);
return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(left, right), parameter);
}
class ReplaceExpressionVisitor : ExpressionVisitor
{
private readonly Expression _oldValue;
private readonly Expression _newValue;
public ReplaceExpressionVisitor(Expression oldValue, Expression newValue)
{
_oldValue = oldValue;
_newValue = newValue;
}
public override Expression Visit(Expression node)
{
if (node == _oldValue)
{
return _newValue;
}
return base.Visit(node);
}
}
}
}
```
@brunobertechini I cant share repo because it's ASPNET Zero based solution.
Is this a separated service?
Yes, Just a reference to [Web.MVC] for depending nothing else.
I just tested this with empty ABP template with sample class inherited [FullAuditedEntity].
In the end, thank you for sharing ALL LINES of AbpDbContext!!!
I reviewed your project completely. I researched about micro-services for two months. I tell you directly that Angular based micro-service is very complicated, to the extent that the Microsoft team that produced the eShopOnContiners sample code has dropped out of its training and provided a brief explanation for the UI based on Angular uses MVC for Identity calls. So in ABP you haven't just Username & Password checker (as eShopOnContiner), you have tenants, roles, features, localization, edition, ...
I believe if you haven't developers team greater of Microsoft eShopOnContainers Team migrate your repository from Angular to MVC you can got that very faster.
Next, you can use SPA for your public Website and fetching your feed data from micro-services DB to your UI where users are just visitors.
Regards
GL
Most helpful comment
Hi all,
I created a sample project and will want to write an article on that. Project repository: https://github.com/aspnetboilerplate/modular-todo-app
What I did:
Current main application has a direct dependency to TodoModule.Web, but it could be easily added as plugin. I added direct dependency just to make it simpler. Actually it does not depend on any class in the module.
Check source code and ask questions if you have. As I said, I'll create an article when I have time for it.