C# integration tests with Entity Framework and async methods - c#

I'm using separate test database for testing buisness logic purposes. My business logic however consists of mainly asynchronous methods which await on other methods multiple times. I'm having problems with testing such methods and I'm running out of ideas what's the cause and how to fix it...
Here's an example of a buisness logic method that I want to test:
public async Task<string> RegisterNewUser(string name, string surname, string address, string city, string phone, string email, string password, string repeatedPassword)
{
string errorMessage = null;
Person newUser = new Person();
// _context is my instance of my class inherited from DBContext
//Getting the next ID for new user
if (_context.People.Any())
newUser.Id = await _context.People.MaxAsync(record => record.Id) + 1;
else
newUser.Id = 0;
newUser.Name = name;
newUser.Surname = surname;
newUser.Address = address;
newUser.City = city;
newUser.Phone = phone;
newUser.Email = email;
newUser.Password = password;
bool validationSuccessful = true;
if (await _context.People.CountAsync(p => p.Email == newUser.Email) > 0)
{
errorMessage = "Given email address is already taken";
validationSuccessful = false;
}
if (validationSuccessful)
{
try
{
// Adding user to database
newUser.Password = GetHashedPassword(newUser.Password);
_context.People.Add(newUser);
// Adding activation info to database
RegistrationActivation act = new RegistrationActivation() { PersonId = newUser.Id, ActivationKey = "blabla"};
_context.RegistrationActivations.Add(act);
await _context.SaveChangesAsync();
}
catch (Exception e)
{
Exception exc = e;
while (exc.InnerException != null)
{
exc = exc.InnerException;
errorMessage = "Exception - " + exc.Message;
}
}
return errorMessage;
}
}
Here's my actual test method:
[TestMethod]
public void Login()
{
Person registered = PersonTestData.CreateGoodTestUser();
string error = UnitOfWork.CrudServices.MyPersonRepository.RegisterNewUser
(registered.Name, registered.Surname, registered.Address, registered.City, registered.Phone, registered.Email, registered.Password, registered.Password).Result;
Assert.IsTrue(error == null, error);
}
UnitOfWork in the code above is just an object instantiated for each test method at the begining and disposed after it completes. It connects with the test database and gives access to buisness logic methods in repositories.
In the current form the test is going to fail with exception in RegisterNewUser with message: Awaiting operation time limit exceeded or sth like this becouse it's translated into my native language...
Now if I comment out the code for adding a user and adding activation info (4 lines right before _context.SaveChangesAsync()) the test will be passed.
Furthermore if I remove all async / await functionality from the RegisterNewUser method - that is not using await and using methods without Async suffix without deleting lines mentioned above - the test is going to be passed as well...
I would be really grateful if somebody could shed some light into this problem.

Stephen Cleary has all the answers you need when it comes to async anything - http://blog.stephencleary.com/2012/02/async-unit-tests-part-2-right-way.html.
Besides Stephen's excellent advice for async unit testing you could also try using XUnit which supports async unit testing methods out of the box e.g.
[Fact]
public async void MyTest()
{
var temp = await SomeAsyncMethod();
}

Related

Microsoft Graph throws Request_ResourceNotFound instead of null/0

I'm an apprentice with 4 months of experience and I got a task to build a holiday request application using data from Microsoft Graph. One of the functions of app is to look up a user'ss manager and display it on the dashboard. Everything was going smooth until my boss logged in. After running Microsoft Graph Query To find out current user Manager, Graph Api returns and error(Request_ResourceNotFound) and breaks whole application instead of returning null or 0. I don't know how to handle that error.
I have tried to return null if the result is null, but that didn't do anything.
This what my controller expects:
var allUsersConnectedToCurrentManagerDisplayName = graphHelper.GetManagerForCurrentUser(userIdToCheck).DisplayName;
var allUsersConnectedToCurrentManagerEmail = graphHelper.GetManagerForCurrentUser(userIdToCheck).UserPrincipalName;
var allUsersConnectedToCurrentManagerId = graphHelper.GetManagerForCurrentUser(userIdToCheck).Id;
Microsoft Graph Helper:
User GetDirectManagerForUser(GraphServiceClient _graphServiceClient, string managerId)
{
using(var task = Task.Run(async() => await _graphServiceClient.Users[managerId].Manager.Request().GetAsync()))
{
while (!task.IsCompleted)
Thread.Sleep(200);
var manager = task.Result as Microsoft.Graph.User;
return manager;
}
}
I was expecting this to return null and just don't display a direct manager for the user without anyone above him.
So you've got a few things going on here.
The first, and the most glaring, issue is that your code is requesting the same User record from Graph three times in a row. Each call you're making to GetDirectManagerForUser is downloading the entire User profile. You want to avoid doing this:
var manager = await graphHelper.GetManagerForCurrentUser(userIdToCheck);
var allUsersConnectedToCurrentManagerDisplayName = manager.DisplayName;
var allUsersConnectedToCurrentManagerEmail = manager.UserPrincipalName;
var allUsersConnectedToCurrentManagerId = manager.Id;
The second issue to avoid is wrapping your request in a Task like that. It adds a lot of complexity to the code, makes it super hard to debug, and isn't necessary. Simply add async Task<> at the method level and let the compiler handle wiring it up for you:
async Task<User> GetDirectManagerForUser(GraphServiceClient _graphServiceClient, string managerId)
Third, your casting the result but not capturing any exceptions (i.e. the 404 your getting). You want to capture these and return an empty User:
var manager = await graphHelper.GetManagerForCurrentUser(userIdToCheck);
var allUsersConnectedToCurrentManagerDisplayName = manager.DisplayName;
var allUsersConnectedToCurrentManagerEmail = manager.UserPrincipalName;
var allUsersConnectedToCurrentManagerId = manager.Id;
async Task<User> GetDirectManagerForUser(GraphServiceClient _graphServiceClient, string managerId)
{
try
{
// Normal path
return await _graphServiceClient
.Users[managerId]
.Manager
.Request()
.GetAsync();
}
catch (Exception)
{
// Something went wrong or no manager exists
var emptyUser = new User();
}
}
You have to catch the exception in order to return null.
I would write the function like this:
public User GetDirectManagerForUser(GraphServiceClient _graphServiceClient, string managerId)
{
//.Result, because this function in synchronious
try
{
var manager = await _graphServiceClient.Users[managerId].Manager.Request().GetAsync().Result;
return manager;
}
catch(Exception)
{
return null;
}
}
You could also make the function async like this:
public async Task<User> GetDirectManagerForUser(GraphServiceClient _graphServiceClient, string managerId)
{
try
{
var manager = await _graphServiceClient.Users[managerId].Manager.Request().GetAsync();
return manager;
}
catch(Exception)
{
return null;
}
}
Why haven't you specified an accessibility level?

Enumerator 'Update' class does not contain a definition, but only for the Unit Testing method and not when it use inside the DAL project

I'm trying to run a unit test for a Update method in a DAO (EmployeeDAO.cs) inside my DAL layer/project. Inside the EmployeeDAO.cs class, my Update method
public UpdateStatus Update(Employee emp)
{
UpdateStatus status = UpdateStatus.Failed;
HelpdeskRepository repo = new HelpdeskRepository(new DbContext());
try
{
DbContext ctx = new DbContext();
var builder = Builders<Employee>.Filter;
var filter = Builders<Employee>.Filter.Eq("Id", emp.Id) & Builders<Employee>.Filter.Eq("Version", emp.Version);
var update = Builders<Employee>.Update
.Set("DepartmentId", emp.DepartmentId)
.Set("Email", emp.Email)
.Set("Firstname", emp.Firstname)
.Set("Lastname", emp.Lastname)
.Set("Phoneno", emp.Phoneno)
.Set("Title", emp.Title)
.Inc("Version", 1);
var result = ctx.Employees.UpdateOne(filter, update);
status = repo.Update(emp.Id.ToString(), filter, update);
//ask how to get status to work in MatchedCount/Modified count so we don't need DbContext use
if (result.MatchedCount == 0) //if zero version didn't match
{
status = UpdateStatus.Stale;
}
else if (result.ModifiedCount == 1)
{
status = UpdateStatus.Ok;
}
else
{
status = UpdateStatus.Failed;
}
}
catch (Exception ex)
{
DALUtils.ErrorRoutine(ex, "EmployeeDAO", "UpdateWithRepo");
}
return status;
}
appears to work fine, with no bugs being detected by the compiler. However, when I try to do some unit testing on it in this method inside my EmployeeDAOTests.cs/UnitTestProject inside the same solution,
[TestMethod]
public void TestUpdateShouldReturnOK()
{
EmployeeDAO dao = new EmployeeDAO();
Employee emp = dao.GetById(eid);
emp.Phoneno = "(555)555-9999";
Assert.IsTrue(dao.Update(emp) == UpdateStatus.OK);
}
it tells me that
(CS0117)"'UpdateStatus' does not contain a definition for 'OK'"
, which can be seen here to quite obviously have a definition for OK that appears to be valid for use in my actual DAO:
public enum UpdateStatus
{
Ok = 1,
Failed = -1,
Stale = -2
};
And on another note, when I trade the order in which I define Ok, Failed, and Stale around, it stops causing Unit Testing errors but begins to cause DAO errors!
Very confusing, anybody have any input?
It was a small case mistake :) UpdateStatus.OK should be UpdateStatus.Ok :)

Correct pattern to call a service containing an async call from an MVC controller

I am new to TAP and async/await in practice in C# so I may have some bad code smells here, so be gentle. :-)
I have a service method that looks as follows:
public OzCpAddOrUpdateEmailAddressToListOutput AddOrUpdateEmailAddressToList(
OzCpAddOrUpdateEmailAddressToListInput aParams)
{
var result = new OzCpAddOrUpdateEmailAddressToListOutput();
try
{
var mailChimManager = new MailChimpManager(aParams.MailChimpApiKey);
Task<Member> mailChimpResult =
mailChimManager.Members.AddOrUpdateAsync(
aParams.Listid,
new Member
{
EmailAddress = aParams.EmailAddress
});
//Poll async task until it completes.
//Give it at most 8 seconds to do what it needs to do
var outOfTime = DateTime.Now.AddSeconds(8);
while (!mailChimpResult.IsCompleted)
{
if (DateTime.Now > outOfTime)
{
throw new Exception("Timed out waiting for MailChimp API.");
}
}
//Should there have been a problem with the call then we raise an exception
if (mailChimpResult.IsFaulted)
{
throw new Exception(
mailChimpResult.Exception?.Message ??
"Unknown mail chimp library error.",
mailChimpResult.Exception);
}
else
{
//Call to api returned without failing but unless we have
//the email address subscribed we have an issue
if (mailChimpResult.Result.Status != Status.Subscribed)
{
throw new Exception(
$"There was a problem subscribing the email address
{aParams.EmailAddress} to the mailchimp list id
{aParams.Listid}");
}
}
}
catch (Exception ex)
{
result.ResultErrors.AddFatalError(PlatformErrors.UNKNOWN, ex.Message);
}
return result;
}
But when I call in from MVC Controller action mailChimpResult.IsCompleted always returns false and eventually I hit the timeout.
I realise this is because I am not chaining the async calls as per HttpClient IsComplete always return false and because of different threads this behaviour is "expected".
However I want my service method to hide the complexity of the async nature of what it is doing and merely do what appears to be a synchronous call in my action method namely:
var mailChimpResult =
_PlatformMailChimpService.AddOrUpdateEmailAddressToList(
new OzCpAddOrUpdateEmailAddressToListInput
{
EmailAddress = aFormCollection["aEmailAddress"],
Listid = ApplicationSettings.Newsletter.MailChimpListId.Value,
MailChimpApiKey = ApplicationSettings.Newsletter.MailChimpApiKey.Value
});
if (mailChimpResult.Result == true)
{
//So something
}
Ideally you should avoid the .Result and .IsFaulted properties of the Task and Task<T> objects, that was code smell number one. When you're using these objects you should use async and await through the entire stack. Consider your service written this way instead:
public async Task<OzCpAddOrUpdateEmailAddressToListOutput>
AddOrUpdateEmailAddressToList(
OzCpAddOrUpdateEmailAddressToListInput aParams)
{
var result = new OzCpAddOrUpdateEmailAddressToListOutput();
try
{
var mailChimManager = new MailChimpManager(aParams.MailChimpApiKey);
Member mailChimpResult =
await mailChimManager.Members.AddOrUpdateAsync(
aParams.Listid,
new Member
{
EmailAddress = aParams.EmailAddress
});
}
catch (Exception ex)
{
result.ResultErrors.AddFatalError(PlatformErrors.UNKNOWN, ex.Message);
}
return result;
}
Notice that I was able to remove all of that unnecessary polling and examining of properties. We mark the method as Task<OzCpAddOrUpdateEmailAddressToListOutput> returning and decorate it with the async keyword. This allows us to use the await keyword in the method body. We await the .AddOrUpdateAsync which yields the Member.
The consuming call into the service looks similar, following the same paradigm of async and await keywords with Task or Task<T> return types:
var mailChimpResult =
await _PlatformMailChimpService.AddOrUpdateEmailAddressToList(
new OzCpAddOrUpdateEmailAddressToListInput
{
EmailAddress = aFormCollection["aEmailAddress"],
Listid = ApplicationSettings.Newsletter.MailChimpListId.Value,
MailChimpApiKey = ApplicationSettings.Newsletter.MailChimpApiKey.Value
});
if (mailChimpResult.Result == true)
{
//So something
}
It is considered best practice to postfix the "Async" word to the method, to signify that it is asynchronous, i.e.; AddOrUpdateEmailAddressToListAsync.

TDD for Web API with NUnit, NSubtitute

I'm still confused with some TDD concepts and how to do it correctly. I'm trying to use it implement for a new project using Web API. I have read a lot about it, and some article suggest NUnit as a testing framework and NSubstitute to mock the repository.
What I don't understand is with NSubstitute we can define the expected result of what we want, is this valid if we want to validate our code logic?
Let's say I have a controller like this with Put and Delete method:
[BasicAuthentication]
public class ClientsController : BaseController
{
// Dependency injection inputs new ClientsRepository
public ClientsController(IRepository<ContactIndex> clientRepo) : base(clientRepo) { }
[HttpPut]
public IHttpActionResult PutClient(string accountId, long clientId, [FromBody] ClientContent data, string userId = "", string deviceId = "", string deviceName = "")
{
var result = repository.UpdateItem(new CommonField()
{
AccountId = accountId,
DeviceId = deviceId,
DeviceName = deviceName,
UserId = userId
}, clientId, data);
if (result.Data == null)
{
return NotFound();
}
if (result.Data.Value != clientId)
{
return InternalServerError();
}
IResult<IDatabaseTable> updatedData = repository.GetItem(accountId, clientId);
if (updatedData.Error)
{
return InternalServerError();
}
return Ok(updatedData.Data);
}
[HttpDelete]
public IHttpActionResult DeleteClient(string accountId, long clientId, string userId = "", string deviceId = "")
{
var endResult = repository.DeleteItem(new CommonField()
{
AccountId = accountId,
DeviceId = deviceId,
DeviceName = string.Empty,
UserId = userId
}, clientId);
if (endResult.Error)
{
return InternalServerError();
}
if (endResult.Data <= 0)
{
return NotFound();
}
return Ok();
}
}
and I create some unit tests like this:
[TestFixture]
public class ClientsControllerTest
{
private ClientsController _baseController;
private IRepository<ContactIndex> clientsRepository;
private string accountId = "account_id";
private string userId = "user_id";
private long clientId = 123;
private CommonField commonField;
[SetUp]
public void SetUp()
{
clientsRepository = Substitute.For<IRepository<ContactIndex>>();
_baseController = new ClientsController(clientsRepository);
commonField = new CommonField()
{
AccountId = accountId,
DeviceId = string.Empty,
DeviceName = string.Empty,
UserId = userId
};
}
[Test]
public void PostClient_ContactNameNotExists_ReturnBadRequest()
{
// Arrange
var data = new ClientContent
{
shippingName = "TestShippingName 1",
shippingAddress1 = "TestShippingAdress 1"
};
clientsRepository.CreateItem(commonField, data)
.Returns(new Result<long>
{
Message = "Bad Request"
});
// Act
var result = _baseController.PostClient(accountId, data, userId);
// Asserts
Assert.IsInstanceOf<BadRequestErrorMessageResult>(result);
}
[Test]
public void PutClient_ClientNotExists_ReturnNotFound()
{
// Arrange
var data = new ClientContent
{
contactName = "TestContactName 1",
shippingName = "TestShippingName 1",
shippingAddress1 = "TestShippingAdress 1"
};
clientsRepository.UpdateItem(commonField, clientId, data)
.Returns(new Result<long?>
{
Message = "Data Not Found"
});
var result = _baseController.PutClient(accountId, clientId, data, userId);
Assert.IsInstanceOf<NotFoundResult>(result);
}
[Test]
public void PutClient_UpdateSucceed_ReturnOk()
{
// Arrange
var postedData = new ClientContent
{
contactName = "TestContactName 1",
shippingName = "TestShippingName 1",
shippingAddress1 = "TestShippingAdress 1"
};
var expectedResult = new ContactIndex() { id = 123 };
clientsRepository.UpdateItem(commonField, clientId, postedData)
.Returns(new Result<long?> (123)
{
Message = "Data Not Found"
});
clientsRepository.GetItem(accountId, clientId)
.Returns(new Result<ContactIndex>
(
expectedResult
));
// Act
var result = _baseController.PutClient(accountId, clientId, postedData, userId)
.ShouldBeOfType<OkNegotiatedContentResult<ContactIndex>>();
// Assert
result.Content.ShouldBe(expectedResult);
}
[Test]
public void DeleteClient_ClientNotExists_ReturnNotFound()
{
clientsRepository.Delete(accountId, userId, "", "", clientId)
.Returns(new Result<int>()
{
Message = ""
});
var result = _baseController.DeleteClient(accountId, clientId, userId);
Assert.IsInstanceOf<NotFoundResult>(result);
}
[Test]
public void DeleteClient_DeleteSucceed_ReturnOk()
{
clientsRepository.Delete(accountId, userId, "", "", clientId)
.Returns(new Result<int>(123)
{
Message = ""
});
var result = _baseController.DeleteClient(accountId, clientId, userId);
Assert.IsInstanceOf<OkResult>(result);
}
}
Looking at the code above, am I writing my unit tests correctly? I feel like I'm not sure how it will validate the logic in my controller.
Please ask for more information, if there is anything that needs clarified.
If the code you've actually posted is a true reflection of your test to code ratio, then you don't appear to be following a TDD approach. One of the core concepts is that you don't write code that hasn't been tested. This means that as a basic rule, you need to have a minimum of one test for every branch in your code, otherwise there would be no reason for the branch to have been written.
Looking at your DeleteClient method there are three branches, so there should be at least three tests for the method (you've only posted two).
// Test1 - If repo returns error, ensure expected return value
DeleteClient_Error_ReturnsInternalError
// Test2 - If repo returns negative data value, ensure expected return value
DeleteClient_NoData_ReturnsNotFound
// Test3 - If repo returns no error, ensure expected return
DeleteClient_Success_ReturnsOk
You can use NSubtitute to redirect your code down these different paths so that they can be tested. So, to redirect down the InternalError branch you would setup your substitute something like this:
clientsRepository.Delete(Args.Any<int>(), Args.Any<int>(),
Args.Any<string>(), Args.Any<string>(),
Args.Any<int>())
.Returns(new Result<int>()
{
Error = SomeError;
});
Without knowing the IRepository interface it's hard to be 100% accurate about the NSubstitute setup, but basically, the above is saying when the Delete method of the substitute is called with the given parameter types (int,int,string,string,int) the substitute should return a value which has Error set to SomeError (this is the trigger for the InternalError branch of logic). You would then assert that when calling the system under test, it returns InternalServerError.
You need to repeat this for each of your logic branches. Don't forget that you'll need to setup the substitute to return all appropriate values for to get to each branch of logic. So, to get to the ReturnsNotFound branch, you'd need to make your repository return NoError and a negative Data value.
I said above, you needed a minimum of one test for each branch of logic. It's a minimum because there are other things that you will want to test. In the above substitute setups, you'll notice that I'm using Args.Any<int> etc. That's because for the behaviour the tests above are interested in, it doesn't really matter if the correct values are being passed to the repository or not. Those tests are testing the logic flows influenced by the return values of the repository. For your testing to be complete, you'll also need to make sure that the correct values are being passed to the repository. Depending on your approach, you might have a test per parameter, or you might have a test to validate all of the parameters in the call to your repository.
To validate all of the parameters, taking the ReturnsInternalError test as a base, you would simply have to add a validation call to the subsistute something like this to validate the parameters:
clientsRepository.Received().Delete(accountId, userId, "", "", clientId);
I'm using the ReturnsInternalError test as a base because after validating the the call, I want to get out of the method under test as fast as possible and in this instance it's by returning an error.
First, when coding in TDD, you must make the smallest functions possible. About three lines of code (excluding brackets and signature) THe function should have only one purpose. Ex: a function called GetEncriptedData should call two other methods GetData and EncryptData instead of getting the data and encrypt it. If your tdd is well done, that shouldn't be a problem to get to that result. When functions are too long, tests are pointless as they can't really cover all your logic. And my tests Use the having when then logic. Ex.: HavingInitialSituationA_WhenDoingB_ThenShouldBecomeC is the name of the test.
You would find three blocks of code inside your test representing these three parts. There is more. When doing tdd, you shoud always do one step at once. If you expect your function to return 2, make a test that validates if it returns two, and make your function literally return 2. Afther that you could want some conditions and test them in other test cases and all your tests should bass at the end. TDD is a completely different way to code. YOu make one test, it fails, you make the necessary code so it pass, and you make another test, it fails... This is my experience, and my way of implementing TDD tells me that you are wrong. But this is my point of view. Hope I helped you.

EF and MVC - approach to work together

I used the following approach long time (approx 5 years):
Create one big class with initialization of XXXEntities in controller and create each method for each action with DB. Example:
public class DBRepository
{
private MyEntities _dbContext;
public DBRepository()
{
_dbContext = new MyEntities();
}
public NewsItem NewsItem(int ID)
{
var q = from i in _dbContext.News where i.ID == ID select new NewsItem() { ID = i.ID, FullText = i.FullText, Time = i.Time, Topic = i.Topic };
return q.FirstOrDefault();
}
public List<Screenshot> LastPublicScreenshots()
{
var q = from i in _dbContext.Screenshots where i.isPublic == true && i.ScreenshotStatus.Status == ScreenshotStatusKeys.LIVE orderby i.dateTimeServer descending select i;
return q.Take(5).ToList();
}
public void SetPublicScreenshot(string filename, bool val)
{
var screenshot = Get<Screenshot>(p => p.filename == filename);
if (screenshot != null)
{
screenshot.isPublic = val;
_dbContext.SaveChanges();
}
}
public void SomeMethod()
{
SomeEntity1 s1 = new SomeEntity1() { field1="fff", field2="aaa" };
_dbContext.SomeEntity1.Add(s1);
SomeEntity2 s2 = new SomeEntity2() { SE1 = s1 };
_dbContext.SomeEntity1.Add(s2);
_dbContext.SaveChanges();
}
And some external code create DBRepository object and call methods.
It worked fine. But now Async operations came in. So, if I use code like
public async void AddStatSimplePageAsync(string IPAddress, string login, string txt)
{
DateTime dateAdded2MinsAgo = DateTime.Now.AddMinutes(-2);
if ((from i in _dbContext.StatSimplePages where i.page == txt && i.dateAdded > dateAdded2MinsAgo select i).Count() == 0)
{
StatSimplePage item = new StatSimplePage() { IPAddress = IPAddress, login = login, page = txt, dateAdded = DateTime.Now };
_dbContext.StatSimplePages.Add(item);
await _dbContext.SaveChangesAsync();
}
}
can be a situation, when next code will be executed before SaveChanged completed and one more entity will be added to _dbContext, which should not be saved before some actions. For example, some code:
DBRepository _rep = new DBRepository();
_rep.AddStatSimplePageAsync("A", "b", "c");
_rep.SomeMethod();
I worry, that SaveChanged will be called after line
_dbContext.SomeEntity1.Add(s1);
but before
_dbContext.SomeEntity2.Add(s2);
(i.e. these 2 actions is atomic operation)
Am I right? My approach is wrong now? Which approach should be used?
PS. As I understand, will be the following stack:
1. calling AddStatSimplePageAsync
2. start calling await _dbContext.SaveChangesAsync(); inside AddStatSimplePageAsync
3. start calling SomeMethod(), _dbContext.SaveChangesAsync() in AddStatSimplePageAsync is executing in another (child) thread.
4. complete _dbContext.SaveChangesAsync() in child thread. Main thread is executing something in SomeMethod()
Ok this time I (think)'ve got your problem.
At first, it's weird that you have two separate calls to SaveChangesmethod. Usually you should try to have it at the end of all your operations and then dispose it.
Even thought yes, your concerns are right, but some clarifications are needed here.
When encountering an asyncor await do not think about threads, but about tasks, that are two different concepts.
Have a read to this great article. There is an image that will practically explain you everything.
To say that in few words, if you do not await an async method, you can have the risk that your subsequent operation could "harm" the execution of the first one. To solve it, simply await it.

Categories

Resources