DEV Community

loading...
Cover image for More Than Just CRUD with .NET Core 3.1

More Than Just CRUD with .NET Core 3.1

_patrickgod profile image Patrick God Updated on ・12 min read

This tutorial series is now also available as an online video course. You can watch the first hour on YouTube or get the complete course on Udemy. Or you just keep on reading. Enjoy! :)

More Than Just CRUD with .NET Core 3.1

Introduction

It is time to let our RPG characters fight.

In the upcoming lectures, we will build a service with functions to let our characters attack another character with a weapon or with one of their skills.

Additionally, we implement a function to let characters fight by themselves until one of them, well, has no hit points anymore.

This will result in victories and defeats which is some kind of highscore we can use to display our RPG characters in a certain order.

So, with no further ado, let the games begin!

Prepare to Fight!

While you weren’t watching, I added some relations between characters, weapons, and skills so that we have characters available to fight against each other. It’s not necessary, but maybe prepare one or two characters yourself so that you can use them for the upcoming implementations.

For now, we’ve got Frodo with the Master Sword and the skill Frenzy, and Raistlin with the Crystal Wand and the skills Fireball and Blizzard. I’m really curious about how they will compete.

Frodo & Raistlin

Master Sword vs. Crystal Wand

Skills overview

Anyways, to count the upcoming fights, victories, and defeats, let’s add exactly these three properties as int to the Character model.

public class Character
{
    public int Id { get; set; }
    public string Name { get; set; } = "Frodo";
    public int HitPoints { get; set; } = 100;
    public int Strength { get; set; } = 10;
    public int Defense { get; set; } = 10;
    public int Intelligence { get; set; } = 10;
    public RpgClass Class { get; set; } = RpgClass.Knight;
    public User User { get; set; }
    public Weapon Weapon { get; set; }
    public List<CharacterSkill> CharacterSkills { get; set; }
    public int Fights { get; set; }
    public int Victories { get; set; }
    public int Defeats { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

We can also add them to the GetCharacterDto.

public class GetCharacterDto
{
    public int Id { get; set; }
    public string Name { get; set; } = "Frodo";
    public int HitPoints { get; set; } = 100;
    public int Strength { get; set; } = 10;
    public int Defense { get; set; } = 10;
    public int Intelligence { get; set; } = 10;
    public RpgClass Class { get; set; } = RpgClass.Knight;
    public GetWeaponDto Weapon { get; set; }
    public List<GetSkillDto> Skills { get; set; }
    public int Fights { get; set; }
    public int Victories { get; set; }
    public int Defeats { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Now let’s add these properties to the database. First, we add a new migration with dotnet ef migrations add FightProperties.

You see that the migration will add three new columns to the Characters table with 0 as the default value. Exactly what we need.

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AddColumn<int>(
        name: "Defeats",
        table: "Characters",
        nullable: false,
        defaultValue: 0);
    migrationBuilder.AddColumn<int>(
        name: "Fights",
        table: "Characters",
        nullable: false,
        defaultValue: 0);
    migrationBuilder.AddColumn<int>(
        name: "Victories",
        table: "Characters",
        nullable: false,
        defaultValue: 0);
}
Enter fullscreen mode Exit fullscreen mode

Let’s update the database with dotnet ef database update.

After the update, we can see the new columns in the SQL Server Management Studio.

Fight properties in SQL Server Management Studio

Next would already be a FightService with the corresponding interface and controller.

We create a new folder FightService and add the C# classes IFightService and FightService which implements the interface.

namespace dotnet_rpg.Services.FightService
{
    public class FightService : IFightService
    {

    }
}
Enter fullscreen mode Exit fullscreen mode

After that, we create the FightController for the web service calls.

We add the ControllerBase class and the [Route()] and [ApiController] attribute but leave out authentication. Of course, it would make sense to add authentication here, but I think you know how to implement it now and for this example, it’s sufficient to just let the RPG characters fight without the permission of their respective owners.

using Microsoft.AspNetCore.Mvc;

namespace dotnet_rpg.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class FightController : ControllerBase
    {

    }
}
Enter fullscreen mode Exit fullscreen mode

We can also already add a constructor to the FightController which injects the FightService.

public class FightController : ControllerBase
{
    private readonly IFightService _fightService;
    public FightController(IFightService fightService)
    {
        _fightService = fightService;
    }
}
Enter fullscreen mode Exit fullscreen mode

Alright, and finally we register the FightService in the Startup.cs file with services.AddScoped().

services.AddScoped<IFightService, FightService>();
Enter fullscreen mode Exit fullscreen mode

Great! So far the preparations. Coming up next, we implement a way to attack a character with a weapon.

Attack with Weapons

The idea behind attacking other characters with a weapon is that we define an attacker and an opponent and use the weapon of the attacker (remember there can only be one weapon) to fight against the opponent. Pretty simple actually.

To start, we need some DTOs for the request and the result. So, let’s create a Fight folder and the C# class WeaponAttackDto which only consists of the AttackerId and the OpponentId.

namespace dotnet_rpg.Dtos.Fight
{
    public class WeaponAttackDto
    {
        public int AttackerId { get; set; }
        public int OpponentId { get; set; }
    }
}
Enter fullscreen mode Exit fullscreen mode

Since the attacker can only have one weapon, we only need to know who the attacker is. No need for a weaponId.

For the response, we create a new C# class called AttackResultDto. We can put anything we want in there. For me, it’s interesting to know the names of the attacker and the opponent, their resulting hit points and the damage that was taken.

namespace dotnet_rpg.Dtos.Fight
{
    public class AttackResultDto
    {
        public string Attacker { get; set; }
        public string Opponent { get; set; }
        public int AttackerHP { get; set; }
        public int OpponentHP { get; set; }
        public int Damage { get; set; }
    }
}
Enter fullscreen mode Exit fullscreen mode

Alright, that’s it for the DTOs. Next, we add a constructor to the FightService class where we only inject the DataContext to access the Characters of the database.

A short side note about that. In a minute we will get certain characters by their Id. We have already implemented that in the CharacterService. So we could actually inject the ICharacterService and use the method GetCharacterById() here. But we would have to use an authenticated user for that which would take a bit more effort during the tests and since this example is supposed to be a bit smaller, we just use the DataContext directly again.

But, don’t get me wrong, please do it that way, if you want to. It’s totally fine if you want to extend this example. I would recommend doing so, because it’s a great exercise, hence a great way to learn.

Anyways, let’s move on with the IFightService interface.

We only need one method. This method returns a ServiceResponse with an AttackResultDto, we call it WeaponAttack() and pass a WeaponAttackDto as request.

using System.Threading.Tasks;
using dotnet_rpg.Dtos.Fight;
using dotnet_rpg.Models;

namespace dotnet_rpg.Services.FightService
{
    public interface IFightService
    {
         Task<ServiceResponse<AttackResultDto>> WeaponAttack(WeaponAttackDto request);
    }
}
Enter fullscreen mode Exit fullscreen mode

Alright, let’s use the automatic implementation of the interface in the FightService real quick, add the async keyword and then we jump to the FightController before we bring the WeaponAttack() method to life.

public async Task<ServiceResponse<AttackResultDto>> WeaponAttack(WeaponAttackDto request)
{
    throw new NotImplementedException();
}
Enter fullscreen mode Exit fullscreen mode

The implementation of the FightController is straightforward.

Similar to the other controllers we have a public async method returning a Task with an IActionResult. The method is called WeaponAttack() and receives a WeaponAttackDto as request. In the body, we return the result of the awaited _fightService.WeaponAttack() method. Last but not least, don’t forget the [HttpPost] attribute with ”Weapon” for the route. Done.

[HttpPost("Weapon")]
public async Task<IActionResult> WeaponAttack(WeaponAttackDto request)
{
    return Ok(await _fightService.WeaponAttack(request));
}
Enter fullscreen mode Exit fullscreen mode

Now we get to the implementation of the WeaponAttack() method of the FightService.

We already did it before, we start with initializing the ServiceResponse, create a try/catch block and send a failing response back in case of an exception.

public async Task<ServiceResponse<AttackResultDto>> WeaponAttack(WeaponAttackDto request)
{
    ServiceResponse<AttackResultDto> response = new ServiceResponse<AttackResultDto>();
    try
    {
    }
    catch (Exception ex)
    {
        response.Success = false;
        response.Message = ex.Message;
    }
    return response;
}
Enter fullscreen mode Exit fullscreen mode

Now, we need the Characters. That’s what I was talking about when I mentioned the sidenote. You could use the CharacterService to receive the attacker or access the _context.Characters directly, only include the Weapon and, of course, find the one Character whose Id matches the request.AttackerId.

Regarding the opponent, we don’t have to include anything, we just need the one Character with the matching request.OpponentId. Be careful, if you want to use the CharacterService here. If the opponent is not a character of the authenticated user, it won’t work, because the GetCharacterById() function only gives you the RPG characters that are related to the user. So, you would have to write another method that doesn’t care about that relation.

Character attacker = await _context.Characters
    .Include(c => c.Weapon)
    .FirstOrDefaultAsync(c => c.Id == request.AttackerId);
Character opponent = await _context.Characters
    .FirstOrDefaultAsync(c => c.Id == request.OpponentId);
Enter fullscreen mode Exit fullscreen mode

We’ve got the fighting characters, great. Now comes the fun part, we calculate the damage. Again, feel free to be creative and use your own formula.

My ingenious formula takes the Damage of the Weapon and adds a random value between 0 and the Strength of the attacker. After that, a random value between 0 and the Defense of the opponent will be subtracted from the damage value. Crazy, huh?

If the damage is above 0 - yes, it could be below if the attacker is not very strong or the opponent has a tremendous armor - we subtract the damage from the HitPoints of the opponent.

And if the HitPoints are below or equal 0, we could return a Message like $"{opponent.Name} has been defeated!";.

int damage = attacker.Weapon.Damage + (new Random().Next(attacker.Strength));
damage -= new Random().Next(opponent.Defense);
if (damage > 0)
    opponent.HitPoints -= (int)damage;
if (opponent.HitPoints <= 0)
    response.Message = $"{opponent.Name} has been defeated!";
Enter fullscreen mode Exit fullscreen mode

After that, we have to make sure to Update() the opponent in the database and also save these changes with SaveChangesAsync().

_context.Characters.Update(opponent);
await _context.SaveChangesAsync();
Enter fullscreen mode Exit fullscreen mode

Finally, we write the response.Data. We simply set the Name and the HitPoints of the attacker and the opponent as well as the damage.

response.Data = new AttackResultDto
{
    Attacker = attacker.Name,
    AttackerHP = attacker.HitPoints,
    Opponent = opponent.Name,
    OpponentHP = opponent.HitPoints,
    Damage = damage
};
Enter fullscreen mode Exit fullscreen mode

That’s it.

public async Task<ServiceResponse<AttackResultDto>> WeaponAttack(WeaponAttackDto request)
{
    ServiceResponse<AttackResultDto> response = new ServiceResponse<AttackResultDto>();
    try
    {
        Character attacker = await _context.Characters
            .Include(c => c.Weapon)
            .FirstOrDefaultAsync(c => c.Id == request.AttackerId);
        Character opponent = await _context.Characters
            .FirstOrDefaultAsync(c => c.Id == request.OpponentId);
        int damage = attacker.Weapon.Damage + (new Random().Next(attacker.Strength));
        damage -= new Random().Next(opponent.Defense);
        if (damage > 0)
            opponent.HitPoints -= (int)damage;
        if (opponent.HitPoints <= 0)
            response.Message = $"{opponent.Name} has been defeated!";
        _context.Characters.Update(opponent);
        await _context.SaveChangesAsync();
        response.Data = new AttackResultDto
        {
            Attacker = attacker.Name,
            AttackerHP = attacker.HitPoints,
            Opponent = opponent.Name,
            OpponentHP = opponent.HitPoints,
            Damage = damage
        };
    }
    catch (Exception ex)
    {
        response.Success = false;
        response.Message = ex.Message;
    }
    return response;
}
Enter fullscreen mode Exit fullscreen mode

To test this, we’ve got Frodo and Raistlin using the Master Sword and a Crystal Wand.

Frodo & Raistlin

Master Sword vs. Crystal Wand

You see, Frodo has better values, but maybe Raistlin has better chances to win throwing some fireballs later.

In Postman we use the URL http://localhost:5000/fight/weapon with POST as the HTTP method. The body consists of the attackerId and the opponentId.

{
    "attackerid" : 5,
    "opponentid" : 4
}
Enter fullscreen mode Exit fullscreen mode

Make sure you have started the application with dotnet watch run and then let’s attack!

{
    "data": {
        "attacker": "Frodo",
        "opponent": "Raistlin",
        "attackerHP": 100,
        "opponentHP": 71,
        "damage": 29
    },
    "success": true,
    "message": null
}
Enter fullscreen mode Exit fullscreen mode

It’s pretty obvious, Frodo is stronger.

If we let Raistlin attack by switching the Ids, we see that the Crystal Wand can’t do much.

{
    "data": {
        "attacker": "Raistlin",
        "opponent": "Frodo",
        "attackerHP": 71,
        "opponentHP": 97,
        "damage": 3
    },
    "success": true,
    "message": null
}
Enter fullscreen mode Exit fullscreen mode

If we let Frodo attack a couple more times, we see that Raistlin is defeated.

{
    "data": {
        "attacker": "Frodo",
        "opponent": "Raistlin",
        "attackerHP": 97,
        "opponentHP": -18,
        "damage": 31
    },
    "success": true,
    "message": "Raistlin has been defeated!"
}
Enter fullscreen mode Exit fullscreen mode

Alright, that was fun! Let’s implement “The Return of the Mage” now by using some skill attacks.

Attack with Skills

First, we add a new DTO called SkillAttackDto with three int members, the AttackerId, the OpponentId and this time also the SkillId. Since a Character can have multiple skills, we want to specify which skill should be used for the attack.

namespace dotnet_rpg.Dtos.Fight
{
    public class SkillAttackDto
    {
        public int AttackerId { get; set; }
        public int OpponentId { get; set; }
        public int SkillId { get; set; }
    }
}
Enter fullscreen mode Exit fullscreen mode

Then we need a new method, of course. In the IFightService interface we add the method SkillAttack() with the SkillAttackDto as request this time.

Task<ServiceResponse<AttackResultDto>> SkillAttack(SkillAttackDto request);
Enter fullscreen mode Exit fullscreen mode

We’ll cover the FightService in a second. Let’s deal with the FightController instead real quick. You already know it. We can just copy the WeaponAttack() method and make a few little changes.

The method is called SkillAttack(), of course, again with the SkillAttackDto as request, we call the corresponding method of the _fightService and change the route to ”Skill”.

[HttpPost("Skill")]
public async Task<IActionResult> SkillAttack(SkillAttackDto request)
{
    return Ok(await _fightService.SkillAttack(request));
}
Enter fullscreen mode Exit fullscreen mode

Regarding the FightService we can copy the WeaponAttack() method as well and make some changes to the new method. First the name and request type again, of course.

Receiving the attacker looks a bit different now. Instead of including the Weapon, we include the CharacterSkills and then the Skill of each CharacterSkill. Again, here’s an opportunity to use the CharacterService. Receiving the opponent stays the same.

Character attacker = await _context.Characters
    .Include(c => c.CharacterSkills).ThenInclude(cs => cs.Skill)
    .FirstOrDefaultAsync(c => c.Id == request.AttackerId);
Character opponent = await _context.Characters
    .FirstOrDefaultAsync(c => c.Id == request.OpponentId);
Enter fullscreen mode Exit fullscreen mode

Now comes something new, we have to get the correct Skill. Since we included the skills of the RPG character, we don’t access the skills via the _context, we just look if the attacker really has that specific skill.

We initialize a new CharacterSkill object and look through the attacker.CharacterSkills to find the one where the Skill.Id of the CharacterSkill equals the request.SkillId.

If we don’t find one, meaning the characterSkill object is null, because the user requested a wrong SkillId, we return a failing response with a message like {attacker.Name} doesn't know that skill..

CharacterSkill characterSkill =
    attacker.CharacterSkills.FirstOrDefault(cs => cs.Skill.Id == request.SkillId);
if (characterSkill == null)
{
    response.Success = false;
    response.Message = $"{attacker.Name} doesn't know that skill.";
    return response;
}
Enter fullscreen mode Exit fullscreen mode

Regarding the damage we go really crazy. We take the same formula but use characterSkill.Skill.Damage, of course, and replace the Strength with the Intelligence of the attacker.

int damage = characterSkill.Skill.Damage + (new Random().Next(attacker.Intelligence));
damage -= new Random().Next(opponent.Defense);
if (damage > 0)
    opponent.HitPoints -= (int)damage;
if (opponent.HitPoints <= 0)
    response.Message = $"{opponent.Name} has been defeated!";
Enter fullscreen mode Exit fullscreen mode

And that’s already it.

public async Task<ServiceResponse<AttackResultDto>> SkillAttack(SkillAttackDto request)
{
    ServiceResponse<AttackResultDto> response = new ServiceResponse<AttackResultDto>();
    try
    {
        Character attacker = await _context.Characters
            .Include(c => c.CharacterSkills).ThenInclude(cs => cs.Skill)
            .FirstOrDefaultAsync(c => c.Id == request.AttackerId);
        Character opponent = await _context.Characters
            .FirstOrDefaultAsync(c => c.Id == request.OpponentId);

        CharacterSkill characterSkill =
            attacker.CharacterSkills.FirstOrDefault(cs => cs.Skill.Id == request.SkillId);
        if (characterSkill == null)
        {
            response.Success = false;
            response.Message = $"{attacker.Name} doesn't know that skill.";
            return response;
        }

        int damage = characterSkill.Skill.Damage + (new Random().Next(attacker.Intelligence));
        damage -= new Random().Next(opponent.Defense);

        if (damage > 0)
            opponent.HitPoints -= (int)damage;

        if (opponent.HitPoints <= 0)
            response.Message = $"{opponent.Name} has been defeated!";

        _context.Characters.Update(opponent);
        await _context.SaveChangesAsync();

        response.Data = new AttackResultDto
        {
            Attacker = attacker.Name,
            AttackerHP = attacker.HitPoints,
            Opponent = opponent.Name,
            OpponentHP = opponent.HitPoints,
            Damage = damage
        };
    }
    catch (Exception ex)
    {
        response.Success = false;
        response.Message = ex.Message;
    }
    return response;
}
Enter fullscreen mode Exit fullscreen mode

Before we test this, make sure to reset the hit points of our opponents.

Frodo & Raistlin

In Postman we use the URL http://localhost:5000/fight/skill this time and add the skillId to the request.

{
    "attackerid" : 5,
    "opponentid" : 4,
    "skillid" : 1
}
Enter fullscreen mode Exit fullscreen mode

When we use a skill the character does not have, we should get our proper error message like Frodo doesn’t know that skill.

{
    "data": null,
    "success": false,
    "message": "Frodo doesn't know that skill."
}
Enter fullscreen mode Exit fullscreen mode

But what happens if Frodo uses the skill Frenzy?

{
    "attackerid" : 5,
    "opponentid" : 4,
    "skillid" : 2
}
Enter fullscreen mode Exit fullscreen mode

He still makes good damage!

{
    "data": {
        "attacker": "Frodo",
        "opponent": "Raistlin",
        "attackerHP": 100,
        "opponentHP": 77,
        "damage": 23
    },
    "success": true,
    "message": null
}
Enter fullscreen mode Exit fullscreen mode

Let’s fight back with Raistlin and his fireballs or even with the mighty Blizzard!

{
    "attackerid" : 4,
    "opponentid" : 5,
    "skillid" : 3
}
Enter fullscreen mode Exit fullscreen mode
{
    "data": {
        "attacker": "Raistlin",
        "opponent": "Frodo",
        "attackerHP": 77,
        "opponentHP": -16,
        "damage": 51
    },
    "success": true,
    "message": "Frodo has been defeated!"
}
Enter fullscreen mode Exit fullscreen mode

That’s what I call a fight! The Blizzard makes a lot of damage and Frodo has been defeated.

Okay, but instead of doing these attacks manually, let’s implement an automatic fight next.


That's it for the 12th part of this tutorial series. I hope it was useful for you. To get notified for the next part, simply follow me here on dev.to or subscribe to my newsletter. You'll be the first to know.

See you next time!

Take care.


Next up: More Than Just CRUD with .NET Core 3.1 - Part 2

Image created by cornecoba on freepik.com.


But wait, there’s more!

Discussion (3)

pic
Editor guide
Collapse
rangiesrule profile image
rangiesrule

Hi Patrick,
It's been a great proccess following and applying to my application, so thank you.

I have created a service and controller to generate calculations like you have the fight with weapon here, however I need to add 'controller' to my route in postman. Do you know why and where I may have gone wrong?

Thank you in advance.
Tyrone

Collapse
rangiesrule profile image
rangiesrule

...feel embarrased now. After returning to the code I find a simple spelling error that caused the issue.

All fixed and working as expected!

Tyrone

Collapse
_patrickgod profile image
Patrick God Author

Hey Tyrone,

Glad you found the fix! :)

Take care,
Patrick