Event-Driven ASP.NET Core Microservice Architectures

Event-Driven ASP.NET Core Microservice Architectures

Hello again, everyone! ??

It's time to share my experience try to explore a Microservices Web Apps with .NET on this project I will use a RabbitMQ as a message broker.

I create two .NET project that have its own database (on this project I use SQLite) and that projects able to communicate using RabbitMQ.

The Architecture

Prerequisites

Before we begin building our microservices web application, let's ensure that we have a all development environment ready.

  • .NET SDK for this project I use v5.0
  • A IDE such as Visual Studio, for this project I using Visual Studio 2019
  • Docker for install and run RabbitMQ easily

Step 1

Launch Visual Studio 2019.

Create two new project, here we will set it as "User Service" and "Post Service"

first let's work on User Service, we can create new Project then set Name and directory.

after the project is created install the necessary packages Dependency > right click Packages > Manage NuGet Packages

then install these packages

after the installing done, create new Folder "Entities" then create two new class on that folder

user.cs
namespace UserService.Entities
{
    public class User
    {
        public int ID { get; set; }
        public string Name { get; set; }
        public string Mail { get; set; }
        public string OtherData { get; set; }
        public int Version { get; set; }
    }
}        
IntegrationEvent.cs
namespace UserService.Entities
{
    public class IntegrationEvent
    {
        public int ID { get; set; }
        public string Event { get; set; }
        public string Data { get; set; }
    }
}        

create new folder "Data" and on here create new class

IntegrationEvent.cs
namespace UserService.Data
{
    public class UserServiceContext : DbContext
    {
        public UserServiceContext(DbContextOptions<UserServiceContext> options)
            : base(options)
        {
        }
        public DbSet<UserService.Entities.IntegrationEvent> IntegrationEventOutbox { get; set; }


        public DbSet<UserService.Entities.User> User { get; set; }
    }
}        

we also need to edit Startup class

Startup .cs
public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

       
        public void ConfigureServices(IServiceCollection services)
        {

            services.AddControllers();
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "UserService", Version = "v1" });
            });

            services.AddDbContext<UserServiceContext>(options =>
                 options.UseSqlite(@"Data Source=user.db"));

            services.AddSingleton<IntegrationEventSenderService>();
            services.AddHostedService<IntegrationEventSenderService>(provider => provider.GetService<IntegrationEventSenderService>());
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, UserServiceContext dbContext)
        {
            if (env.IsDevelopment())
            {
                dbContext.Database.EnsureCreated();
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "UserService v1"));
            }

            app.UseRouting();
...        

then finally we just need to create new Controller

UserController.cs
namespace UserService.Controllers

{
    [Route("api/[controller]")]
    [ApiController]
    public class UsersController : ControllerBase
    {
        private readonly UserServiceContext _context;

        private readonly IntegrationEventSenderService _integrationEventSenderService;

        public UsersController(UserServiceContext context, IntegrationEventSenderService integrationEventSenderService)
        {
            _context = context;
            _integrationEventSenderService = integrationEventSenderService;
        }

        [HttpGet]
        public async Task<ActionResult<IEnumerable<User>>> GetUser()
        {
            return await _context.User.ToListAsync();
        }

        [HttpPut("{id}")]
        public async Task<IActionResult> PutUser(int id, User user)
        {
            using var transaction = _context.Database.BeginTransaction();

            _context.Entry(user).State = EntityState.Modified;
            await _context.SaveChangesAsync();

            var integrationEventData = JsonConvert.SerializeObject(new
            {
                id = user.ID,
                newname = user.Name,
                version = user.Version
            });
            _context.IntegrationEventOutbox.Add(
                new IntegrationEvent()
                {
                    Event = "user.update",
                    Data = integrationEventData
                });

            _context.SaveChanges();
            transaction.Commit();
            _integrationEventSenderService.StartPublishingOutstandingIntegrationEvents();
            return CreatedAtAction("PutUser", new { id = user.ID }, user);
        }

        [HttpPost]
        public async Task<ActionResult<User>> PostUser(User user)
        {
            user.Version = 1;
            using var transaction = _context.Database.BeginTransaction();
            _context.User.Add(user);
            _context.SaveChanges();
            
            var integrationEventData = JsonConvert.SerializeObject(new
            {
                id = user.ID,
                name = user.Name,
                version = user.Version
            });
            _context.IntegrationEventOutbox.Add(
                new IntegrationEvent()
                {
                    Event = "user.add",
                    Data = integrationEventData
                });
            _context.SaveChanges();
            transaction.Commit();

            _integrationEventSenderService.StartPublishingOutstandingIntegrationEvents();


            return CreatedAtAction("GetUser", new { id = user.ID }, user);
        }
    }
}        

now we've finish for the user service, this service allow us to create, edit and get User, the database will generated if we run this service.

to make the apps go debug mode right click on the solution then click Properties

set it into multiple startup project.


Step 2

Similar will we do on Post Service, after creating the project we must create new "Entity" Folder, then make two new class.

User.cs
namespace PostService.Entities
{
    public class User
    {
        public int ID { get; set; }
        public string Name { get; set; }
        public int Version { get; set; }
    }
}        
Post.cs
namespace PostService.Entities
{
    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }

        public int UserId { get; set; }
        public User User { get; set; }
    }
}        

later create PostServiceContext class on Data Folder.

PostServiceContext .cs
namespace PostService.Data
{
    public class PostServiceContext : DbContext
    {
        public PostServiceContext(DbContextOptions<PostServiceContext> options)
            : base(options)
        {
        }

        public DbSet<PostService.Entities.Post> Post { get; set; }
        public DbSet<PostService.Entities.User> User { get; set; }
    }
}        

simillar with the user service we also need to edit Startup Class to add a database setup

Startup.cs
public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {

            services.AddControllers();
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "PostService", Version = "v1" });
            });

            services.AddDbContext<PostServiceContext>(options =>
                options.UseSqlite(@"Data Source=post.db"));
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, PostServiceContext dbContext)
        {
            if (env.IsDevelopment())
            {
                dbContext.Database.EnsureCreated();
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "PostService v1"));
            }

            app.UseRouting();
...        

next we need to edit the Program class, here we put setup about RabbitMQ

Program.cs
namespace PostService
{
    public class Program
    {
        public static void Main(string[] args)
        {
            ListenForIntegrationEvents();
            CreateHostBuilder(args).Build().Run();
        }

        private static void ListenForIntegrationEvents()
        {
            var factory = new ConnectionFactory();
            var connection = factory.CreateConnection();
            var channel = connection.CreateModel();
            var consumer = new EventingBasicConsumer(channel);

            consumer.Received += (model, ea) =>
            {
                var contextOptions = new DbContextOptionsBuilder<PostServiceContext>()
                    .UseSqlite(@"Data Source=post.db")
                    .Options;
                var dbContext = new PostServiceContext(contextOptions);
                var body = ea.Body.ToArray();
                var message = Encoding.UTF8.GetString(body);
                Console.WriteLine(" [x] Received {0}", message);
                var data = JObject.Parse(message);
                var type = ea.RoutingKey;
                if (type == "user.add")
                {
                    if (dbContext.User.Any(a => a.ID == data["id"].Value<int>()))
                    {
                        Console.WriteLine("Ignoring old/duplicate entity");
                    }
                    else
                    {
                        dbContext.User.Add(new User()
                        {
                            ID = data["id"].Value<int>(),
                            Name = data["name"].Value<string>(),
                            Version = data["version"].Value<int>()
                        });
                        dbContext.SaveChanges();
                    }
                }
                else if (type == "user.update")
                {
                    
                    int newVersion;
                    if (data.TryGetValue("version", out JToken versionToken) && versionToken.Type == JTokenType.Integer)
                    {
                        newVersion = versionToken.Value<int>();
                    }
                    else
                    {
                        // Handle the case where "version" key is missing or not an integer
                        Console.WriteLine("Ignoring message with missing or non-integer 'version' key");
                        return;
                    }


                    var user = dbContext.User.First(a => a.ID == data["id"].Value<int>());
                    if (user.Version >= newVersion)
                    {
                        Console.WriteLine("Ignoring old/duplicate entity");
                    }
                    else
                    {
                        user.Name = data["newname"].Value<string>();
                        user.Version = newVersion;
                        dbContext.SaveChanges();
                    }
                }
                channel.BasicAck(ea.DeliveryTag, false);
            };
            channel.BasicConsume(queue: "user.postservice",
                                     autoAck: false,
                                     consumer: consumer);
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}        

Lastly we need to create Post Controller class, we just make a Get and Post endpoints.

PostController.cs
namespace PostService.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class PostController : ControllerBase
    {
        private readonly PostServiceContext _context;

        public PostController(PostServiceContext context)
        {
            _context = context;
        }

        [HttpGet]
        public async Task<ActionResult<IEnumerable<Post>>> GetPost()
        {
            return await _context.Post.Include(x => x.User).ToListAsync();
        }

        [HttpPost]
        public async Task<ActionResult<Post>> PostPost(Post post)
        {
            _context.Post.Add(post);
            await _context.SaveChangesAsync();

            return CreatedAtAction("GetPost", new { id = post.PostId }, post);
        }
    }
}        

also don't forget to set the solution properties.

Step 3

now we just need to set the message broker, we will install it on docker

run the command bellow on terminal

docker run -d  -p 15672:15672 -p 5672:5672 --hostname cute-rabbit --name some-rabbit rabbitmq:3-management        

that command will pull and install "rabbitmq:3-management" from docker hub then expose it on port 15672 and 5672, 15672 is used for accessing web UI and 5672 will be use by our apps, then run it on container "cute-rabbit" we can check this cheat sheet for learn more about docker command.

if the installation runs without problem we can see the image running on docker desktop

go to https://localhost:15672 to access web UI rabbitMQ, we will be asked to login, use guest as username and password (this is the default setting, we can change it latter)


there we can see a dashboard about all necessary information, go to Queues and Streams

create new Queue user.postservice and user.otherservice


then go to Exchanges and create new exchange "user" change the type into fanout


then bind the queue to the exchange

for more information about RabbitMQ we an check the documentation

okay, then we can test our Services.

Step 4

run the user service and post service, then try create new user we can use swagger or postman

there we can se that the User Service sending a message to RabbitMQ, to check that the data is updated on Post service we can check the table on 'post.db' that generated, then try to create a Post

There we can see that we successfully create new post that contain user from User Service. next we can test if we edit user on User Service

then we can check our previous post, the user is also getting updated


In this article, we've embarked on a journey to build a microservices web application using .NET and RabbitMQ, and we've achieved some minimal microservices services, for real project we will need to working on security and optimization, you can check my Github repo for the complete code ?

Thank you for your attention.

I hope this toturial is useful See you ??






要查看或添加评论,请登录

Feri Ginanjar的更多文章

  • Workflow Engine with .NET

    Workflow Engine with .NET

    Hello, I'm Feri, and today I'm excited to share my hands-on experience with workflow engines. In the dynamic world of…

  • Full Stack CRUD web API with Angular ASP.NET

    Full Stack CRUD web API with Angular ASP.NET

    Hello again, everyone! ?? It's time to share my experience try exploring create a Web Apps with .NET and Angular…

  • ASP.NET Core MVC with React

    ASP.NET Core MVC with React

    Hello this time I will try to share my exploration make Application ASP.NET Core MVC with React, Prerequisites .

  • Create Simple CRUD with .NET Core and SQL Server

    Create Simple CRUD with .NET Core and SQL Server

    Hello I'm Feri ?? on this article I want to share my experience creating simple CRUD with .NET Core and SQL Server.

社区洞察

其他会员也浏览了