I'm currently writing integration tests using nunit for a previously untested server that was written in C# using ApiController and Entity Framework. Most of the tests run just fine, but I've ran into two that always cause the database to time out. The error messages look something like this:
System.Data.Entity.Infrastructure.DbUpdateException : An error occurred while updating the entries. See the inner exception for details.
System.Data.Entity.Core.UpdateException : An error occurred while updating the entries. See the inner exception for details.
System.Data.SqlClient.SqlException : Timeout expired. The timeout period elapsed prior to completion of the operation or the server is not responding.
System.ComponentModel.Win32Exception : The wait operation timed out
The first test that's timing out:
[TestCase, WithinTransaction]
public async Task Patch_EditJob_Success()
{
var testJob = Data.SealingJob;
var requestData = new Job()
{
ID = testJob.ID,
Name = "UPDATED"
};
var apiResponse = await _controller.EditJob(testJob.ID, requestData);
Assert.IsInstanceOf<StatusCodeResult>(apiResponse);
Assert.AreEqual("UPDATED", testJob.Name);
}
The other test that's timing out:
[TestCase, WithinTransaction]
public async Task Post_RejectJob_Success()
{
var rejectedJob = Data.SealingJob;
var apiResponse = await _controller.RejectJob(rejectedJob.ID);
Assert.IsInstanceOf<OkResult>(apiResponse);
Assert.IsNull(rejectedJob.Organizations);
Assert.AreEqual(rejectedJob.JobStatus, JobStatus.OnHold);
_fakeEmailSender.Verify(
emailSender => emailSender.SendEmail(rejectedJob.Creator.Email, It.Is<string>(emailBody => emailBody.Contains(rejectedJob.Name)), It.IsAny<string>()),
Times.Once());
}
These are the controller methods that these tests are using:
The timeout always happens on the first call to await db.SaveChangesAsync() within the controller. Other controller methods that are being tested also call SaveChangesAsync without any problem. I've also tried calling SaveChangesAsync from within the failing tests and it works fine there. Both of these methods they are calling work normally when called from within the controller, but time out when called from the tests.
[HttpPatch]
[Route("editjob/{id}")]
public async Task<IHttpActionResult> EditJob(int id, Job job)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (id != job.ID)
{
return BadRequest();
}
Job existingJob = await db.Jobs
.Include(databaseJob => databaseJob.Regions)
.FirstOrDefaultAsync(databaseJob => databaseJob.ID == id);
existingJob.Name = job.Name;
// For each Region find if it already exists in the database
// If it does, use that Region, if not one will be created
for (var i = 0; i < job.Regions.Count; i++)
{
var regionId = job.Regions[i].ID;
var foundRegion = db.Regions.FirstOrDefault(databaseRegion => databaseRegion.ID == regionId);
if (foundRegion != null)
{
existingJob.Regions[i] = foundRegion;
db.Entry(existingJob.Regions[i]).State = EntityState.Unchanged;
}
}
existingJob.JobType = job.JobType;
existingJob.DesignCode = job.DesignCode;
existingJob.DesignProgram = job.DesignProgram;
existingJob.JobStatus = job.JobStatus;
existingJob.JobPriority = job.JobPriority;
existingJob.LotNumber = job.LotNumber;
existingJob.Address = job.Address;
existingJob.City = job.City;
existingJob.Subdivision = job.Subdivision;
existingJob.Model = job.Model;
existingJob.BuildingDesignerName = job.BuildingDesignerName;
existingJob.BuildingDesignerAddress = job.BuildingDesignerAddress;
existingJob.BuildingDesignerCity = job.BuildingDesignerCity;
existingJob.BuildingDesignerState = job.BuildingDesignerState;
existingJob.BuildingDesignerLicenseNumber = job.BuildingDesignerLicenseNumber;
existingJob.WindCode = job.WindCode;
existingJob.WindSpeed = job.WindSpeed;
existingJob.WindExposureCategory = job.WindExposureCategory;
existingJob.MeanRoofHeight = job.MeanRoofHeight;
existingJob.RoofLoad = job.RoofLoad;
existingJob.FloorLoad = job.FloorLoad;
existingJob.CustomerName = job.CustomerName;
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!JobExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return StatusCode(HttpStatusCode.NoContent);
}
[HttpPost]
[Route("{id}/reject")]
public async Task<IHttpActionResult> RejectJob(int id)
{
var organizations = await db.Organizations
.Include(databaseOrganization => databaseOrganization.Jobs)
.ToListAsync();
// Remove job from being shared with organizations
foreach (var organization in organizations)
{
foreach (var organizationJob in organization.Jobs)
{
if (organizationJob.ID == id)
{
organization.Jobs.Remove(organizationJob);
}
}
}
var existingJob = await db.Jobs.FindAsync(id);
existingJob.JobStatus = JobStatus.OnHold;
await db.SaveChangesAsync();
await ResetJob(id);
var jobPdfs = await DatabaseUtility.GetPdfsForJobAsync(id, db);
var notes = "";
foreach (var jobPdf in jobPdfs)
{
if (jobPdf.Notes != null)
{
notes += jobPdf.Name + ": " + jobPdf.Notes + "\n";
}
}
// Rejection email
var job = await db.Jobs
.Include(databaseJob => databaseJob.Creator)
.SingleAsync(databaseJob => databaseJob.ID == id);
_emailSender.SendEmail(
job.Creator.Email,
job.Name + " Rejected",
notes);
return Ok();
}
Other code that might be relevant:
The model being used is just a normal code-first Entity Framework class:
public class Job
{
public Job()
{
this.Regions = new List<Region>();
this.ComponentDesigns = new List<ComponentDesign>();
this.MetaPdfs = new List<Pdf>();
this.OpenedBy = new List<User>();
}
public int ID { get; set; }
public string Name { get; set; }
public List<Region> Regions { get; set; }
// etc...
}
To keep the database clean between tests, I'm using this custom attribute to wrap each one in a transaction (from http://tech.trailmax.info/2014/03/how-we-do-database-integration-tests-with-entity-framework-migrations/):
public class WithinTransactionAttribute : Attribute, ITestAction
{
private TransactionScope _transaction;
public ActionTargets Targets => ActionTargets.Test;
public void BeforeTest(ITest test)
{
_transaction = new TransactionScope();
}
public void AfterTest(ITest test)
{
_transaction.Dispose();
}
}
The database connection and controller being tested is build in setup methods before each test:
[TestFixture]
public class JobsControllerTest : IntegrationTest
{
// ...
private JobsController _controller;
private Mock<EmailSender> _fakeEmailSender;
[SetUp]
public void SetupController()
{
this._fakeEmailSender = new Mock<EmailSender>();
this._controller = new JobsController(Database, _fakeEmailSender.Object);
}
// ...
}
public class IntegrationTest
{
protected SealingServerContext Database { get; set; }
protected TestData Data { get; set; }
[SetUp]
public void SetupDatabase()
{
this.Database = new SealingServerContext();
this.Data = new TestData(Database);
}
// ...
}
This bug was apparently caused by the use of await within a TransactionScope. Following the top answer to this question, I added the TransactionScopeAsyncFlowOption.Enabled parameter when constructing the TransactionScope and the timeout issue went away.
Related
I am trying to unit test our DB layer's stored procedures/functions using OrmLite's ScalarAsync(), for example, with the PostgreSQL dialect. (I tried using SqlLite in-memory but it doesn't do stored procedures or functions.)
I found some hints in the unit-tests for OrmLite on GitHub as well as an article which points to them.
Here's what I have:
[Fact]
public async Task TestMyMethod_CallsMyStoredProcedure()
{
// Arrange
Mock<IDashboardDbConnectionFactory> mockConnFac = new();
MockDialectProvider prov = new();
prov.ExecFilter = new MockStoredProcExecFilter();
OrmLiteConnectionFactory dbFactory =
new(
"User ID=asdf;Password=asdf;Host=localhost;Port=5432;Database=asdf;Pooling=true;Connection Lifetime=0;",
prov, false);
OrmLiteConfig.ExecFilter = new MockStoredProcExecFilter();
mockConnFac.Setup(m => m.OpenAsync(It.IsAny<ISecurityContext>()))
.Returns(async () =>
{
OrmLiteConnection asdf = new(dbFactory);
OrmLiteConfig.ExecFilter = new MockStoredProcExecFilter();
await asdf.OpenAsync();
return asdf;
});
mockConnFac.Setup(m => m.Open(It.IsAny<ISecurityContext>()))
.Returns(() =>
{
OrmLiteConnection asdf = new(dbFactory);
OrmLiteConfig.ExecFilter = new MockStoredProcExecFilter();
asdf.Open();
return asdf;
});
// Act
MyDataLayerCLass target = new(mockConnFac.Object, new NullLoggerFactory());
bool test1 =
await target.ExecMyStoredProcAsync(new Mock<ISecurityContext>().Object, Guid.NewGuid());
// Assert
Assert.True(test1);
}
private class MockDialectProvider : PostgreSqlDialectProvider
{
public MockDialectProvider()
{
base.ExecFilter = new MockStoredProcExecFilter();
}
public new IOrmLiteExecFilter ExecFilter { get; set; } = new MockStoredProcExecFilter();
}
private class MockStoredProcExecFilter : OrmLiteExecFilter
{
public override T Exec<T>(IDbConnection dbConn, Func<IDbCommand, T> filter)
{
try
{
T val = base.Exec(dbConn, filter);
if (dbConn.GetLastSql() == "select central_data.my_stored_function(#UserId, #ParentId)")
return (T)(object)true;
return val;
}
catch (Exception)
{
if (dbConn.GetLastSql() == "select central_data.my_stored_function(#UserId, #ParentId)")
return (T)(object)true;
throw;
}
}
public override async Task<T> Exec<T>(IDbConnection dbConn, Func<IDbCommand, Task<T>> filter)
{
try
{
// This is where the breakpoint hits. Returns false b/c the ids
// don't match actual records in the DB.
T val = await base.Exec(dbConn, filter);
if (dbConn.GetLastSql() == "select central_data.my_stored_function(#UserId, #ParentId)")
return (T)(object)true;
return val;
}
catch (Exception)
{
string sql = dbConn.GetLastSql();
if (sql == "select central_data.my_stored_function(#UserId, #ParentId)")
{
return (T)(object)true;
}
throw;
}
}
}
The problem is that it requires a valid connection to a valid database. So it's really an integration test when what I want is a unit test. Is there a way to run the dialect provider without an open connection?
I have a class as below :
public class CosmosRepository: ICosmosRepository
{
private ICosmoDBSettings cosmoDbSettings;
private CosmosClient cosmosClient;
private static readonly object syncLock = new object();
// The database we will create
private Database database;
public CosmosRepository(ICosmoDBSettings cosmoDBSettings)
{
this.cosmoDbSettings = cosmoDBSettings;
this.InitializeComponents();
}
private void InitializeComponents()
{
try
{
if (cosmosClient != null)
{
return;
}
lock (syncLock)
{
if (cosmosClient != null)
{
return;
}
this.cosmosClient = new CosmosClient(
cosmoDbSettings.CosmosDbUri
, cosmoDbSettings.CosmosDbAuthKey
, new CosmosClientOptions
{
ConnectionMode = ConnectionMode.Direct
}
);
this.database = this.cosmosClient.CreateDatabaseIfNotExistsAsync(cosmoDbSettings.DocumentDbDataBaseName).Result;
}
}
catch (Exception ex)
{
throw ex;
}
}
}
I have my repository method as:
Don't bother about hardcoded values.
public async Task<Employee> GetById()
{
var container = this.database.GetContainer("Employees");
var document = await container.ReadItemAsync<Employee>("44A85B9E-2522-4BDB-891A-
9EA91F6D4CBF", new PartitionKey("PartitionKeyValue"));
return document.Response;
}
Note
How to write Unit Test(MS Unit tests) in .NET Core with respect to Cosmos Database?
How to mock CosmosClient and all its methods.
Could someone help me with this issue?
My UnitTests looks like:
public class UnitTest1
{
private Mock<ICosmoDBSettings> cosmoDbSettings;
private Mock<CosmosClient> cosmosClient;
private Mock<Database> database;
[TestMethod]
public async Task TestMethod()
{
this.CreateSubject();
var databaseResponse = new Mock<DatabaseResponse>();
var helper = new CosmosDBHelper(this.cosmoDbSettings.Object);
this.cosmosClient.Setup(d => d.CreateDatabaseIfNotExistsAsync("TestDatabase", It.IsAny<int>(), It.IsAny<RequestOptions>(), It.IsAny<CancellationToken>())).ReturnsAsync(databaseResponse.Object);
var mockContainer = new Mock<Container>();
this.database.Setup(x => x.GetContainer(It.IsAny<string>())).Returns(mockContainer.Object);
var mockItemResponse = new Mock<ItemResponse<PortalAccount>>();
mockItemResponse.Setup(x => x.StatusCode).Returns(It.IsAny<HttpStatusCode>);
var mockPortalAccount = new PortalAccount { PortalAccountGuid = Guid.NewGuid() };
mockItemResponse.Setup(x => x.Resource).Returns(mockPortalAccount);
mockContainer.Setup(c => c.ReadItemAsync<PortalAccount>(It.IsAny<string>(),It.IsAny<PartitionKey>(), It.IsAny<ItemRequestOptions>(), It.IsAny<CancellationToken>())).ReturnsAsync(mockItemResponse.Object);
var pas = helper.GetById().Result;
Assert.AreEqual(pas.PortalAccountGuid, mockPortalAccount.PortalAccountGuid);
}
public void CreateSubject(ICosmoDBSettings cosmoDBSettings = null)
{
this.cosmoDbSettings = cosmoDbSettings ?? new Mock<ICosmoDBSettings>();
this.cosmoDbSettings.Setup(x => x.CosmosDbUri).Returns("https://localhost:8085/");
this.cosmoDbSettings.Setup(x => x.CosmosDbAuthKey).Returns("C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==");
this.cosmoDbSettings.Setup(x => x.DocumentDbDataBaseName).Returns("TestDatabase");
this.database = new Mock<Database>();
this.cosmosClient = new Mock<CosmosClient>();
}
}
Note:
Exception:
Response status code does not indicate success: 404 Substatus: 0 Reason: (Microsoft.Azure.Documents.DocumentClientException: Message: {"Errors":["Resource Not Found"]}
I'm not creating a document. directly I'm fetching the document because I'm returning the mock response only. Is it correct??
How would I go about modifying existing code to call other controllers stored dictionary information? (without recalling the db multiple times, but just once at the start of the rest api's life).
Atm (I think) am storing the information in a dictionary (PipeMaterials) correctly. Now I'm lost on how to go about getting the information out to other controller.
Controller storing information
Controller wanting to consume information
Storing
public class MaterialsController : ControllerBase
{
public Dictionary<int, Materials> PipeMaterials;
public Dictionary<int, Rank> Ranks;
public Dictionary<int, Sumps> Sumps;
private readonly UMMClient23Context _context;
public MaterialsController(UMMClient23Context context)
{
_context = context;
LoadMaterials();
}
public void LoadMaterials()
{
PipeMaterials = new Dictionary<int, Materials>();
Task<MaterialsObjects> task = GetMaterials();
var result = task.Result;
foreach (var item in result.Ummmaterials)
{
if (!PipeMaterials.TryAdd(item.MaterialsId, item))
{
Console.Error.WriteLine("Could not load material: " + item.MaterialsName);
}
}
}
// GET: api/Materials
[HttpGet]
public async Task<MaterialsObjects> GetMaterials()
{
MaterialsObjects returnable = new MaterialsObjects();
returnable.Ummmaterials = await _context.Materials.ToListAsync();
return returnable;
}
// GET: api/MaterialDescription/5
[HttpGet("{materialsId}")]
public string GetMaterialDescription(int materialsId)
{
Materials item;
if (PipeMaterials.TryGetValue(materialsId, out item))
{
return item.MaterialsName;
}
else
{
return null;
}
//var materials = _context.Materials
// .Where(m=> m.MaterialsId == materialsId)
// .Select(m => m.MaterialsName)
// .FirstOrDefault();
}
Consuming
public class PipeController : ControllerBase
{
MaterialsController materialsController;
UMMDBHelper uMMDBHelper;
private readonly UMMClient23Context _context;
public PipeController(UMMClient23Context context)
{
_context = context;
uMMDBHelper = new UMMDBHelper(context);
}
//GET: api/Pipe
[HttpGet]
public async Task<ActionResult<IEnumerable<Data>>> Get(string value)
{
return await _context.Data.ToListAsync();
}
// GET: api/Pipe/{assestNumber}
[HttpGet("{assetNumber}")] // Make return into Object
public PipeListObject GetPipe(string assetNumber)
{
PipeListObject returnable = new PipeListObject();
Pipe Pipe = uMMDBHelper.GetPipe(assetNumber);
returnable.UmmPipes.Add(Pipe);
return returnable;
}
//GET: api/PipeMaterial/{materialId}
[HttpGet("{materialId}")]
public string GetPipeMaterial(int materialId)
{
var desc = materialsController.GetMaterialDescription(materialId);
return desc;
}
You could create a new MaterialsController directly in PipeController like
var desc = new MaterialsController(_context).GetMaterialDescription(materialId);
Or you could use HttpClient like
[HttpGet("{materialId}")]
public string GetPipeMaterial(int materialId)
{
using (var client = new HttpClient())
{
var response = await client.GetAsync("https://localhost:44317/api/MaterialDescription/5");
if (response.IsSuccessStatusCode)
{
var result = response.Content.ReadAsStringAsync().Result;
return result;
}
}
return "";
}
I have problem with insert and update by Generic Repository, in generic insert or update it hasnot problem but in many to many relation i am getting error at insert :
An entity object cannot be referenced by multiple instances of IEntityChangeTracker.
update:
The relationship between the two objects cannot be defined because they are attached to different ObjectContext objects.
my codes are
Interface
public interface IGenericRepository<TEntity>:IDisposable
{
void Insert(TEntity entity);
void Update(TEntity entity);
}
Generic class
public class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : class
{
private ApplicationDbContext context=null;
private DbSet<TEntity> dbSet=null;
public GenericRepository()
{
this.context = new ApplicationDbContext();
this.dbSet = context.Set<TEntity>();
}
public GenericRepository(ApplicationDbContext context)
{
this.context = context;
this.dbSet = context.Set<TEntity>();
}
public virtual void Insert(TEntity entity)
{
Error is here---> this.context.Set<TEntity>().Add(entity);
// dbSet.Add(entity);
context.SaveChanges();
}
public virtual void Update(TEntity entity)
{
Error is here---> dbSet.Attach(entity);
context.Entry(entity).State = EntityState.Modified;
context.SaveChanges();
}
}
and control codes are
private IGenericRepository<Blog> _Repository = null;
private IGenericRepository<BlogTag> _RepositoryTag = null;
private IGenericRepository<BlogCategory> _RepositoryCategory = null;
public BlogsController()
{
this._Repository = new GenericRepository<Blog>(new DbContext());
this._RepositoryTag = new GenericRepository<BlogTag>(new DbContext());
this._RepositoryCategory = new GenericRepository<BlogCategory>(new DbContext());
}
public async Task<ActionResult> Create([Bind(Include = "BlogID,BlogTitle,BlogContent,VisitCount,Preview")] Blog blog
,string[] SelectedTags,string[] SelectedCategories, HttpPostedFileBase files)
{
if (SelectedTags != null)
{
blog.BlogTags = new List<BlogTag>();
foreach (var tag in SelectedTags)
{
var tagToAdd = _RepositoryTag.GetById(int.Parse(tag));
blog.BlogTags.Add(tagToAdd);
}
}
if (SelectedCategories != null)
{
blog.BlogCategories = new List<BlogCategory>();
foreach (var cat in SelectedCategories)
{
var catToAdd = _RepositoryCategory.GetById(int.Parse(cat));
blog.BlogCategories.Add(catToAdd);
}
}
if (ModelState.IsValid)
{
blog.DateTimeInsert = DateTime.UtcNow;
blog.DateTimeModify = DateTime.UtcNow;
blog.ImagePath= files != null ? Path.GetFileName(files.FileName) : "";
blog.BlogContent = HttpUtility.HtmlEncode(blog.BlogContent);
_Repository.Insert(blog);
return RedirectToAction("Index");
}
ViewBag.BlogTags = new SelectList(_RepositoryTag.Get(), "BlogTagID", "TagName");
ViewBag.BlogCategories = new SelectList(_RepositoryCategory.Get(), "BlogCategoryID", "CategoriesName");
return View(blog);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit([Bind(Include = "BlogID,BlogTitle,BlogContent,VisitCount,Preview")] Blog blog
, string[] SelectedTags, string[] SelectedCategories, HttpPostedFileBase files)
{
if (Request["BlogID"] == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
int id = int.Parse(Request["BlogID"].ToString());
var blogsToUpdate = _Repository.Query(i => i.BlogID == id, null).Include(t => t.BlogTags).Include(t => t.BlogCategories).Single();
if (TryUpdateModel(blogsToUpdate, "",
new string[] { "BlogID", "BlogTitle", "BlogContent", "VisitCount","Preview" }))
{
try
{
UpdateInstructorCourses(SelectedTags, SelectedCategories, blogsToUpdate);
blogsToUpdate.DateTimeModify = DateTime.UtcNow;
blogsToUpdate.DateTimeInsert = DateTime.UtcNow;
blogsToUpdate.BlogContent = HttpUtility.HtmlEncode(blogsToUpdate.BlogContent);
await _Repository.UpdateAsync(blogsToUpdate, d => d.BlogTitle, d => d.VisitCount, d => d.BlogContent, d => d.ImagePath);
return RedirectToAction("Index");
}
catch (RetryLimitExceededException /* dex */)
{
//Log the error (uncomment dex variable name and add a line here to write a log.
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
}
}
AssignedDDLCHKBoxValues(blogsToUpdate);
return View(blogsToUpdate);
}
private void UpdateInstructorCourses(string[] SelectedTags, string[] SelectedCategories, Blog blogsToUpdate)
{
if (SelectedTags == null)
{
blogsToUpdate.BlogTags = new List<BlogTag>();
return;
}
if (SelectedCategories == null)
{
blogsToUpdate.BlogCategories = new List<BlogCategory>();
return;
}
var SelectedTagsHS = new HashSet<string>(SelectedTags);
var SelectedCategoriesHS = new HashSet<string>(SelectedCategories);
var blogTags = new HashSet<int>(blogsToUpdate.BlogTags.Select(c => c.BlogTagID));
foreach (var tag in _RepositoryTag.Get())
{
if (SelectedTagsHS.Contains(tag.BlogTagID.ToString()))
{
if (!blogTags.Contains(tag.BlogTagID))
{
blogsToUpdate.BlogTags.Add(tag);
}
}//if
else
{
if (blogTags.Contains(tag.BlogTagID))
{
blogsToUpdate.BlogTags.Remove(tag);
}
}//else
}//foreach tag
var blogcategories = new HashSet<int>
(blogsToUpdate.BlogCategories.Select(c => c.BlogCategoryID));
foreach (var Category in _RepositoryCategory.Get())
{
if (SelectedCategoriesHS.Contains(Category.BlogCategoryID.ToString()))
{
if (!blogcategories.Contains(Category.BlogCategoryID))
{
blogsToUpdate.BlogCategories.Add(Category);
}
}//if
else
{
if (blogcategories.Contains(Category.BlogCategoryID))
{
blogsToUpdate.BlogCategories.Remove(Category);
}
}//else
}//foreach skill
}
Your problem is that you are using multiple contexts to work with a single Entity.
On your controller constructor you have these lines:
this._Repository = new GenericRepository<Blog>(new DbContext());
this._RepositoryTag = new GenericRepository<BlogTag>(new DbContext());
this._RepositoryCategory = new GenericRepository<BlogCategory>(new DbContext());
Here, you are creating 3 repositories 9that are supposed to work together) with 3 different contexts.
After that, you proceed to read from the RepositoryTag repository, here:
var tagToAdd = _RepositoryTag.GetById(int.Parse(tag));
When you do this, the object tagToAdd becomes attached to the context inside RepositoryTag. If you debug your list BlogTags where you add this tagToAdd you will see that you have a dynamic proxy, which means that that objetc is attached to a context.
After that, you use another context to fill repository category, here:
var catToAdd = _RepositoryCategory.GetById(int.Parse(cat));
blog.BlogCategories.Add(catToAdd);
Now, your blog object has references to 2 different contexts: the one you used to load the tags (RepositoryTag), and the one you used to load the blog category (RepositoryCategory).
Finally, you try to inser the blog usgin a third context:
_Repository.Insert(blog);
This will throw an exception because EF can't work with multiple contexts like this.
To solve this problem, simply instantiate a context before the repositories, and pass it to all yout repositories, like this:
this.context = new DbContext(); // The context you need to use for all operations you are performing here.
this._Repository = new GenericRepository<Blog>(this.context);
this._RepositoryTag = new GenericRepository<BlogTag>(this.context);
this._RepositoryCategory = new GenericRepository<BlogCategory>(this.context);
Now, don't forget you should dispose your contexts. This is why the most recommended and common approach is to use a code like this:
using (var ctx = new DbContext()) {
var repo = new GenericRepository<Blog>(ctx);
var repoTag = new GenericRepository<BlogTag>(ctx);
var repoCategory = new GenericRepository<BlogCategory>(ctx);
<the rest of your code where you build the `blog` object>
ctx.SaveChanges();
}
I have a base class called ServicePluginBase that implements logging.
public class PluginLog
{
public int Id { get; set; }
public int? ServiceId { get; set; }
public string Event { get; set; }
public string Details { get; set; }
public DateTime DateTime { get; set; }
public string User { get; set; }
}
public class SQLPluginLogger : IPluginLogger
{
//EFLogginContext maps PluginLog like so:
// modelBuilder.Entity<PluginLog>().ToTable("log").HasKey(l => l.Id)
private EFLoggingContext _logger = new EFLoggingContext();
public IQueryable<PluginLog> LogItems
{
get { return _logger.LogItems; }
}
public void LogEvent(PluginLog item)
{
_logger.LogItems.Add(item);
_logger.SaveChanges();
}
}
public abstract class ServicePluginBase : IPlugin
{
private IPluginLogger _logger;
public ServicePluginBase(IPluginLogger logger)
{
_logger = logger;
}
protected LogEvent(string eventName, string details)
{
PluginLog _event = new PluginLog()
{
ServiceId = this.Id,
Event = eventName,
Details = details,
User = Thread.CurrentPrincipal.Identity.Name,
DateTime = DateTime.Now
};
_logger.LogEvent(_event);
}
}
Now, within my concrete class, I log events as they happen. In one class, I have some asynchronous methods running -- and logging. Sometimes this works great. Other times, I get the error stating that "Property 'Id' is part of the object's key and cannot be updated." Interestingly enough, I have absolutely no code that updates the value of Id and I never do Updates of log entries -- I only Add new ones.
Here is the async code from one of my plugins.
public class CPTManager : ServicePluginBase
{
public override async Task HandlePluginProcessAsync()
{
...
await ProcessUsersAsync(requiredUsersList, currentUsersList);
}
private async Task ProcessUsersAsync(List<ExtendedUser> required, List<ExtendedUser> current)
{
using (var http = new HttpClient())
{
var removals = currentUsers.Where(cu => !required.Select(r => r.id).Contains(cu.id)).ToList();
await DisableUsersAsync(removals http);
await AddRequiredUsersAsync(requiredUsers.Where(ru => ru.MustAdd).ToList());
}
}
private async Task DisableUsersAsync(List<ExtendedUser> users, HttpClient client)
{
LogEvent("Disable Users","Total to disable: " + users.Count());
await Task.WhenAll(users.Select(async user =>
{
... //Http call to disable user via Web API
string status = "Disable User";
if(response.status == "ERROR")
{
EmailFailDisableNotification(user);
status += " - Fail";
}
LogEvent(statusText, ...);
if(response.status != "ERROR")
{
//Update FoxPro database via OleDbConnection (not EF)
LogEvent("ClearUDF", ...);
}
}));
}
private async Task AddRequiredUsersAsync(List<ExtendedUser> users, HttpClient client)
{
LogEvent("Add Required Users", "Total users to add: " + users.Count());
await Task.WhenAll(users.Select(async user =>
{
... //Http call to add user via web API
LogEvent("Add User", ...);
if(response.status != "ERROR")
{
//Update FoxPro DB via OleDBConnection (not EF)
LogEvent("UDF UPdate",...);
}
}));
}
}
First, I'm confused why I'm getting the error mentioned above -- "Id can't be updated" ... I'm not populating the Id field nor am I doing updates to the Log file. There are no related tables -- just the single log table.
My guess is that I'm doing something improperly in regards to asynchronous processing, but I'm having trouble seeing it if I am.
Any ideas as to what may be going on?