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

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

_patrickgod profile image Patrick God Updated on ・11 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 (continued)

Start a Fight

Again we start with new DTOs. First, we create a FightRequestDto with only one property, a List of int values for the CharacterIds. Do you see the possibilities here? We could not only let two RPG characters fight against each other, but we could also start a deathmatch!

using System.Collections.Generic;

namespace dotnet_rpg.Dtos.Fight
{
    public class FightRequestDto
    {
        public List<int> CharacterIds { get; set; }
    }
}

The result of the automatic fight or deathmatch should also look a bit different. How about some kind of fighting log that will simply tell us with some sentences how the battle took place?

We create a new FightResultDto class, again with only one property, a List of string values. This will be our Log and we can already initialize this List so that we can easily add new entries right away later.

using System.Collections.Generic;

namespace dotnet_rpg.Dtos.Fight
{
    public class FightResultDto
    {
        public List<string> Log { get; set; } = new List<string>();
    }
}

Off to the IFightService interface. We add a new method called Fight() which returns the FightResultDto and takes a FightRequestDto as request.

Task<ServiceResponse<FightResultDto>> Fight(FightRequestDto request);

Regarding the FightController we can copy another method again, replace the method name as well as the DTO and the method of the _fightService, and we can also remove the route so that this method becomes the default POST call of this controller.

[HttpPost]
public async Task<IActionResult> Fight(FightRequestDto request)
{
    return Ok(await _fightService.Fight(request));
}

Now it’s getting interesting. Let’s create the Fight() method in the FightService. We implement the interface and add the async keyword.

Then we initialize the ServiceResponse and also initialize the Data with a new FightResultDto object.

ServiceResponse<FightResultDto> response = new ServiceResponse<FightResultDto>
{
    Data = new FightResultDto()
};

Next, we add our default try/catch block as always and return a failing response in case of an error.

Inside the try-block, we want to grab all the given RPG characters. Remember, this could also be a battle with dozens of fighters - sounds great, huh?

To get the characters we access the _context as usual, include the Weapon, the CharacterSkills and the Skill and then we use the function Where to get all the characters from the database that match the given IDs. We do that with Where(c => request.CharacterIds.Contains(c.Id)) and then turn the result into a List with ToListAsync().

List<Character> characters =
    await _context.Characters
    .Include(c => c.Weapon)
    .Include(c => c.CharacterSkills).ThenInclude(cs => cs.Skill)
    .Where(c => request.CharacterIds.Contains(c.Id)).ToListAsync();

Alright, we’ve got our fighters. Now we define a boolean variable called defeated, set it to false and start two loops - a while loop and a foreach. The while loop stops when the first character is defeated. The idea behind the foreach is that every character will attack in order.

bool defeated = false;
while (!defeated)
{
    foreach (Character attacker in characters)
    {

    }
}

Inside the foreach loop we grab all the opponents of the attacker first. We do that with the help of the Where() function again by simply filtering all characters that don’t have the Id of the attacker.

Then we randomly choose one opponent. We do that with new Random().Next() and then pass the opponents.Count. That way, we get a random number we can use as an index for the opponents list.

List<Character> opponents = characters.Where(c => c.Id != attacker.Id).ToList();
Character opponent = opponents[new Random().Next(opponents.Count)];

Next, we declare two variables we will use for the resulting log. The damage which is 0 and attackUsed as an empty string. You guessed it, I want to see the used weapon or skill of every single attack.

int damage = 0;
string attackUsed = string.Empty;

The next step is to decide if the attacker uses his weapon or one of his skills. To do that, we define a boolean variable useWeapon and throw a die again with new Random().Next(2). If the result is 0 we choose the weapon, if not, we choose a skill.

bool useWeapon = new Random().Next(2) == 0;
if (useWeapon)
{
}
else
{
}

Now we can set the name of the used attack and then already calculate the damage. Since we did that already in the WeaponAttack() method, let’s extract this part and create a new method we can reuse. We can do that by selecting the code, open the quick-fix menu and choose Extract method.

Extract method

We can call this new method DoWeaponAttack() for instance.

private static int DoWeaponAttack(Character attacker, Character opponent)
{
    int damage = attacker.Weapon.Damage + (new Random().Next(attacker.Strength));
    damage -= new Random().Next(opponent.Defense);
    if (damage > 0)
        opponent.HitPoints -= (int)damage;
    return damage;
}

So now we can use this method and pass the attacker and the opponent to get the damage value and already decrease the HitPoints of the opponent.

if (useWeapon)
{
    attackUsed = attacker.Weapon.Name;
    damage = DoWeaponAttack(attacker, opponent);
}

Regarding the damage for a Skill we can also extract the calculation from the SkillAttack() method and call this method DoSkillAttack(). You see that we have to pass an attacker, an opponent and a characterSkill this time.

private static int DoSkillAttack(Character attacker, Character opponent, CharacterSkill characterSkill)
{
    int damage = characterSkill.Skill.Damage + (new Random().Next(attacker.Intelligence));
    damage -= new Random().Next(opponent.Defense);
    if (damage > 0)
        opponent.HitPoints -= (int)damage;
    return damage;
}

Great. So, we can use this new method for the damage, but first, we have to get a CharacterSkill.

Again we choose one randomly from the list of CharacterSkills of our attacker. We do that with new Random().Next(attacker.CharacterSkills.Count). This will be the index of our CharacterSkill we store in the variable randomSkill.

With this randomSkill variable, we can now set the name of the attackUsed by setting the Skill.Name and finally calculate the damage with the DoSkillAttack() method and give this method the attacker, the opponent and the attacker.CharacterSkill[randomSkill].

else
{
    int randomSkill = new Random().Next(attacker.CharacterSkills.Count);
    attackUsed = attacker.CharacterSkills[randomSkill].Skill.Name;
    damage = DoSkillAttack(attacker, opponent, attacker.CharacterSkills[randomSkill]);
}

Alright, we’ve got our attacks.

That’s all the information we need to add this attack to the log. I would like to add a sentence that looks like “attacker.Name attacks opponent.Name using attackUsed with damage damage” where the damage value has to be above or equal 0. Nice.

response.Data.Log.Add($"{attacker.Name} attacks {opponent.Name} using {attackUsed} with {(damage >= 0 ? damage : 0)} damage.");

We are almost done. Now we have to decide what to do if an opponent has been defeated. So, in case the opponent.HitPoints are 0 or less we set the variable defeated to true. We increase the Victories of the attacker and the Defeats of the opponent. We can add new sentences to our log! Something like “opponent.Name has been defeated!” and “attacker.Name wins with attacker.HitPoints HP left!”. And finally, we stop the fight and leave the loop with break.

if (opponent.HitPoints <= 0)
{
    defeated = true;
    attacker.Victories++;
    opponent.Defeats++;
    response.Data.Log.Add($"{opponent.Name} has been defeated!");
    response.Data.Log.Add($"{attacker.Name} wins with {attacker.HitPoints} HP left!");
    break;
}

After that, we increase the Fights value of all characters in another forEach and also reset the HitPoints for the next fight.

characters.ForEach(c =>
{
    c.Fights++;
    c.HitPoints = 100;
});

And then we update all characters in the database with _context.Characters.UpdateRange(characters) and save everything as usual with SaveChangesAsync().

_context.Characters.UpdateRange(characters);
await _context.SaveChangesAsync();

And that’s it! The automatic fight may begin!

public async Task<ServiceResponse<FightResultDto>> Fight(FightRequestDto request)
{
    ServiceResponse<FightResultDto> response = new ServiceResponse<FightResultDto>
    {
        Data = new FightResultDto()
    };
    try
    {
        List<Character> characters =
            await _context.Characters
            .Include(c => c.Weapon)
            .Include(c => c.CharacterSkills).ThenInclude(cs => cs.Skill)
            .Where(c => request.CharacterIds.Contains(c.Id)).ToListAsync();

        bool defeated = false;
        while (!defeated)
        {
            foreach (Character attacker in characters)
            {
                List<Character> opponents = characters.Where(c => c.Id != attacker.Id).ToList();
                Character opponent = opponents[new Random().Next(opponents.Count)];

                int damage = 0;
                string attackUsed = string.Empty;

                bool useWeapon = new Random().Next(2) == 0;
                if (useWeapon)
                {
                    attackUsed = attacker.Weapon.Name;
                    damage = DoWeaponAttack(attacker, opponent);
                }
                else
                {
                    int randomSkill = new Random().Next(attacker.CharacterSkills.Count);
                    attackUsed = attacker.CharacterSkills[randomSkill].Skill.Name;
                    damage = DoSkillAttack(attacker, opponent, attacker.CharacterSkills[randomSkill]);
                }

                response.Data.Log.Add($"{attacker.Name} attacks {opponent.Name} using {attackUsed} with {(damage >= 0 ? damage : 0)} damage.");

                if (opponent.HitPoints <= 0)
                {
                    defeated = true;
                    attacker.Victories++;
                    opponent.Defeats++;
                    response.Data.Log.Add($"{opponent.Name} has been defeated!");
                    response.Data.Log.Add($"{attacker.Name} wins with {attacker.HitPoints} HP left!");
                    break;
                }
            }
        }

        characters.ForEach(c =>
        {
            c.Fights++;
            c.HitPoints = 100;
        });

        _context.Characters.UpdateRange(characters);
        await _context.SaveChangesAsync();
    }
    catch (Exception ex)
    {
        response.Success = false;
        response.Message = ex.Message;
    }
    return response;
}

But before we start a fight, let’s prepare a third character, so that we actually get a deathmatch.

We already got Sam. In the SQL Server Management Studio, we can give him a new weapon directly. Maybe the Sting with a damage value of 10.

Weapons

Also, we can add a new Skill, like an Iceball which makes 15 damage.

Skills

Then we add the skills Frenzy and Iceball to Sam in the CharacterSkills table.

CharacterSkills

Alright, let’s make sure that every character has 100 hit points and then we can start.

Character values

The URL in Postman is http://localhost:5000/fight/, the HTTP method is POST and the body can already be an array of three characterIds.

{
    "characterids" : [2,4,5]
}

Let’s fight!

{
    "data": {
        "log": [
            "Sam attacks Frodo using Sting with 19 damage.",
            "Raistlin attacks Frodo using Fireball with 33 damage.",
            "Frodo attacks Raistlin using The Master Sword with 28 damage.",
            "Sam attacks Raistlin using Sting with 19 damage.",
            "Raistlin attacks Frodo using Crystal Wand with 7 damage.",
            "Frodo attacks Sam using The Master Sword with 17 damage.",
            "Sam attacks Raistlin using Frenzy with 21 damage.",
            "Raistlin attacks Frodo using Crystal Wand with 6 damage.",
            "Frodo attacks Raistlin using Frenzy with 19 damage.",
            "Sam attacks Frodo using Iceball with 21 damage.",
            "Raistlin attacks Frodo using Blizzard with 51 damage.",
            "Frodo has been defeated!",
            "Raistlin wins with 13 HP left!"
        ]
    },
    "success": true,
    "message": null
}

And that’s how a deathmatch looks like. A beautiful use of different weapons and skills. You see the whole course of the fight.

Feel free to start a couple of more battles, so that the Victories and Defeats of the RPG characters change.

Highscore

We will use these values to display a highscore next.

Highscore: Sort & Filter Entities

To receive a highscore or a ranking of all RPG characters that have ever participated in a fight, we need a GET method and a new DTO for the result. A request object is not necessary.

Let’s start with the DTO. In the Fight folder, we create the C# class HighscoreDTO. The properties I would like to see are the Id of the character, the Name and of course the number of Fights, Victories and Defeats.

namespace dotnet_rpg.Dtos.Fight
{
    public class HighscoreDTO
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Fights { get; set; }
        public int Victories { get; set; }
        public int Defeats { get; set; }
    }
}

We will use AutoMapper to map the Character information to the HighscoreDTO later, so let’s create a new map in our AutoMapperProfile class.

CreateMap<Character, HighscoreDTO>();

Okay. Next, we add a new method to the IFightService interface called GetHigscore() which doesn’t take any arguments but it returns a ServiceResponse with a List of HighscoreDTO instances.

Task<ServiceResponse<List<HighscoreDTO>>> GetHighscore();

In the FightController, we also add the new GetHighscore() method with no parameter and even no attribute, because this will be our default GET call. We simply use the method _fightService.GetHighscore() and that’s it.

public async Task<IActionResult> GetHighscore()
{
    return Ok(await _fightService.GetHighscore());
}

Now the FightService. First, we inject the IMapper to be able to map the characters to the HighscoreDTO. We initialize the field from this parameter, add the underscore and also the AutoMapper using directive.

private readonly DataContext _context;
private readonly IMapper _mapper;
public FightService(DataContext context, IMapper mapper)
{
    _mapper = mapper;
    _context = context;
}

Then we can implement the interface and start writing the code for the GetHighscore() method. We start with the async keyword.

Then, we want the characters, of course. In this example we only want to see the characters that have participated in a fight, so Where the Fights value is greater than 0.

And then we want to see a ranking, so we use OrderByDescending() to order the characters by their Victories. If the number of Victories should be the same for some characters, we can then order by their Defeats ascending by using ThenBy(). In the end, we turn the result to a List.

List<Character> characters = await _context.Characters
    .Where(c => c.Fights > 0)
    .OrderByDescending(c => c.Victories)
    .ThenBy(c => c.Defeats)
    .ToListAsync();

Now we can already create the response. If you want, you can use var for that. It’s controversial whether using var is a best practice or not. I think in the case of initializing an object of a long type name, it is okay to do so. In the end, it’s still strongly typed.

Anyways, we initialize the ServiceResponse and already set the Data by mapping all characters to a HighscoreDTO. We do that with characters.Select() and then use the _mapper with the Map() function for every character and turn that result also into a List.

var response = new ServiceResponse<List<HighscoreDTO>>
{
    Data = characters.Select(c => _mapper.Map<HighscoreDTO>(c)).ToList()
};

And finally we return the response. Done!

public async Task<ServiceResponse<List<HighscoreDTO>>> GetHighscore()
{
    List<Character> characters = await _context.Characters
        .Where(c => c.Fights > 0)
        .OrderByDescending(c => c.Victories)
        .ThenBy(c => c.Defeats)
        .ToListAsync();
    var response = new ServiceResponse<List<HighscoreDTO>>
    {
        Data = characters.Select(c => _mapper.Map<HighscoreDTO>(c)).ToList()
    };
    return response;
}

Testing this is simple. In Postman we use the URL http://localhost:5000/fight/ and this time the HTTP method is GET. Hitting “Send” returns our honorable fighters in the right order.

{
    "data": [
        {
            "id": 4,
            "name": "Raistlin",
            "fights": 30,
            "victories": 14,
            "defeats": 9
        },
        {
            "id": 5,
            "name": "Frodo",
            "fights": 30,
            "victories": 11,
            "defeats": 11
        },
        {
            "id": 2,
            "name": "Sam",
            "fights": 30,
            "victories": 5,
            "defeats": 10
        }
    ],
    "success": true,
    "message": null
}

Beautiful!

Summary

Well, congratulations! You have completed the whole course. You can be proud of yourself. I certainly am.

You learned how to build a Web API with all CRUD operations using the HTTP methods GET, POST, PUT and DELETE in .NET Core.

After implementing all these operations you learned how to store your entities and save your changes in a SQL Server database with Entity Framework Core. You utilized code-first migration for that.

But not everybody should have access to your entities. That’s why you added authentication to your web service. Users have to register and authenticate to get access to their data.

Additionally, you not only learned how to hash a password and verify it again, but you also implemented the use of JSON web tokens. With the help of that token, the user doesn’t have to send her credentials with every call. The token does the job for us.

Then you covered advanced relationships in Entity Framework Core and the SQL Server database. By the example of users, RPG characters, weapons, and skills, you implemented one-to-one, one-to-many and many-to-many relationships.

And finally, you got creative by letting your RPG characters fight against each other and find the best of them all.

I hope you’ve got many many insights and new skills for yourself.

Now it’s up to you. Be creative, use your new knowledge and build something amazing!

Good luck & happy coding! :)


That's it for the last part of this tutorial series. I hope it was useful for you. For more tutorials and online courses, 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.


Image created by cornecoba on freepik.com.


But wait, there’s more!

Discussion

pic
Editor guide
Collapse
bibek100 profile image
Bibek

Could you explain to me about that while loop?
You first defined it to be false but while you were running that while loop why did you use !defeated and how was that loop running even you defined it as false in the first place?

Collapse
_patrickgod profile image
Patrick God Author

Hi Bibek,
Sure. The while loop runs as long as defeated is set to false - pay attention to the ! in the condition that checks if defeated is set to false, which is the case in the beginning.
As soon as one character has been defeated, we set the variable to true, and the fight stops. Makes sense?

Hope this helps.

Take care,
Patrick

Collapse
bibek100 profile image
Bibek

I got it. I thought !defeated will be changed to true after doing this since it was declared false for the first time

Thread Thread
bibek100 profile image
Bibek

Anyway, your whole course is great. Do you have other courses related to .net and react

Thread Thread
_patrickgod profile image
Patrick God Author

Thank you very much! :)

I'm currently working on a .NET 5 and a Blazor course. Would that be of interest to you?

Take care,
Patrick

Thread Thread
bibek100 profile image
Bibek

Yeah I am really interested!! Are you gonna post about it pretty soon?

Thread Thread
_patrickgod profile image
Collapse
awonglk profile image
Anthony Wong

Seriously educational. With abundant video based courses around, i didn't expect to enjoy following a blog/article series based course. I ended up following it all the way to the end of the course! Hope you'll make more courses like this!

Thank you so much!

Collapse
kamandsaadati profile image
KamandSaadati

OMG!
Done! Finally :)
I've been waiting soooo long for this moment to say "Hooray!" for myself for successfully finishing this amazing tutorial and also say a BIG "THANK YOU!", for the second time to you Patrick!
Amazing job in this tutorial!
Cause I personally think like, I've started as an absolute novice in .NET Core and SQL Server and now I'm a junior web developer that has successfully built an API, connected her app to a real DB and managed all the crucial operations between her web app and database, used JWT for authentication and even has developed a further feature for fighting some RPG characters =)
And that's great!
But, I also have a question from you, that I'd really appreciate your answer to it: Where should I go for more learning in the Back-end development area with this technology stack next?

Collapse
_patrickgod profile image
Patrick God Author

Congratulations!! :)

I'm really glad you completed the whole series.

To your question: What do you want to build?
Think about expanding your web application or build a new one. That way you will find new challenges yourself and it's a good way to dive even deeper.

Apart from that, the online course is coming soon. ;) If you have any suggestions, please don't hesitate to tell me.

Take care,
Patrick

Collapse
kamandsaadati profile image
KamandSaadati

Yeah! Nice idea to keep going forward, thank you :) 👍
And for your coming soon course, I'm sure it'd be great just like this one, but for more suggestions, I will make sure that you'd be aware of them, whenever a new idea would come to my mind : D cause your tutorial is already well-structured enough for any beginner looking for some resources to start his/her journey!

Collapse
diggerplays profile image
Digger Plays

Hi Patrick.

Thank you so much for taking the time to put this tutorial series together. The content is excellent and really well presented. I'm an experienced developer of some 30 years, but having been away from the coalface of ASP.NET for a couple of years this is exactly what I needed to get up to speed with .NET core and APIs in particular. The bonus was an insight into EF Core as well.

Great work .. really appreciated.
Si.

Collapse
novinyo profile image
Kodjo Novinyo AMEGADJE

This is dope. I truly enjoyed every bit of it. You're awesome

Collapse
_patrickgod profile image
Patrick God Author

Thank you very much! Glad you like it. :)

Collapse
hadilepanda profile image
Hadi

Really valuable course, everything is clearly explained and I learned a lot from it! I'm grateful for being able to follow along, hope to see more content like this :)

Collapse
_patrickgod profile image
Patrick God Author

Thank you very much! :)

I'm currently working on the video version of this course, so stay tuned. ;)

Anything else you would like to see?

Take care & stay healthy,
Patrick

Collapse
hadilepanda profile image
Hadi

Nice! :) I'd suggest for the video that you keep it pretty much the same format as in this blog:
-> each step first explained by the logic flow and with images etc..
-> then the completed code for that part with an iteration on how the logic is applied.
Going through the steps with the necessary explaination and application would make a great playlist of small and informative videos in my opinion :p

I'd personally love to see more rpg & networking related logics no matter what the language is. To learn there's nothing that I love more than seeing code applied in a rpg theme!

Collapse
jtorres33 profile image
jtorres33

This tutorial was amazing!!! Thanks a lot. You are clarify me a lot themes about Web Api Projects.

Collapse
terrerox profile image
terrerox

How can i build the frontend? U have another tutorial? by the way, thank you! thats a great tutorial!

Collapse
_patrickgod profile image
Patrick God Author

Thank you! :)

Anything specific you're thinking about? Angular, Blazor, anything else?

Take care,
Patrick

Collapse
terrerox profile image
terrerox

I thinking about Blazor, that's something new to me, i'm excited about this technology

Thread Thread
_patrickgod profile image
Patrick God Author

Great! I was thinking about Blazor, too. Stay tuned. ;)

Collapse
rlclaro profile image
Reynier Lester Claro Escalona

thank you very much for this excellent course, you were able to create the repository with the code on github, thanks and regards