IRazorViewEngine.FindView with GetView can't find view - asp.net-core

In my asp.net core project I'm trying to find Razor view using this method:
private IView FindView(ActionContext actionContext, string viewName)
{
var getViewResult = _viewEngine.GetView(executingFilePath: null, viewPath: viewName, isMainPage: true);
if (getViewResult.Success)
{
return getViewResult.View;
}
var findViewResult = _viewEngine.FindView(actionContext, viewName, isMainPage: true);
if (findViewResult.Success)
{
return findViewResult.View;
}
var searchedLocations = getViewResult.SearchedLocations.Concat(findViewResult.SearchedLocations);
var errorMessage = string.Join(
Environment.NewLine,
new[] { $"Unable to find view '{viewName}'. The following locations were searched:" }.Concat(searchedLocations));
throw new InvalidOperationException(errorMessage);
}
where
viewName = "Views/Email/ResetPassword.cshtml"
and _viewEngine is IRazorViewEngine, but it doesn't find any.
My project structure:
IView.FindView method is called from Business.
I also have another project, that have the project structure and uses the same method for retrieving views and, more importantly, it finds this view, but it uses netcoreapp2.2, and my current project uses netcoreapp3.1 (Microsoft.AspNetCore.Mvc.Razor versions are the same - 2.2.0).
Why can't this method find views on .net core 3.1?
UPDATE
Both projects copy this Views folder to Api\bin\Debug\netcoreapp{version} folder on build.

Though I was building things from scratch in Core 3.1 and not upgrading from an earlier version, I ran into the same issue. I got things working by the doing the following:
I created an implementation of IWebHostEnvironment (I called mine DummyWebHostEnvironment.cs). I left all but one of the interface's properties with the default implementation; for that one property, I used the name of the project containing the views. (I just hardcoded it into the sample below for brevity; there are obviously slicker ways to obtain it.)
public class DummyWebHostEnvironment : IWebHostEnvironment
{
public IFileProvider WebRootFileProvider { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
public string WebRootPath { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
public string ApplicationName { get => "TheProjectContainingMyViews.RazorClassLibrary"; set => throw new System.NotImplementedException(); }
public IFileProvider ContentRootFileProvider { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
public string ContentRootPath { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
public string EnvironmentName { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
}
Note: As is evident from the above code, the project containing the Views is a RazorClassLibrary. (I was using this and this as guidesfor getting the RazorViewEngine to work in a console application.)
One I had the implementation above, I added it to my services collection along with some other goodies:
private static RazorViewToStringRenderer GetRenderer()
{
var services = new ServiceCollection();
var applicationEnvironment = PlatformServices.Default.Application;
services.AddSingleton(applicationEnvironment);
var appDirectory = Directory.GetCurrentDirectory();
var environment = new DummyWebHostEnvironment();
services.AddSingleton<IWebHostEnvironment>(environment);
services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
var diagnosticSource = new DiagnosticListener("Microsoft.AspNetCore");
services.AddSingleton<DiagnosticSource>(diagnosticSource);
services.AddSingleton<DiagnosticListener>(diagnosticSource);
services.AddLogging();
services.AddMvc();
services.AddSingleton<RazorViewToStringRenderer>();
var provider = services.BuildServiceProvider();
return provider.GetRequiredService<RazorViewToStringRenderer>();
}
Note: See the first of the links above for the code for RazorViewToStringRenderer. Here's the interface:
public interface IRazorViewToStringRenderer
{
Task<string> RenderViewToStringAsync<TModel>(string viewName, TModel model);
}
Then, in Program.cs, I can just do something like this:
static async Task Main(string[] args)
{
var dto = BuildDto();
var renderer = GetRenderer();
var renderedString = await renderer.RenderViewToStringAsync("Views/Path/To/Some.cshtml", dto);
// ...
}

I had the same issue. Although I am writing in .NET Core 5 already.
I am assuming you are writing based on this or similar solution: https://scottsauber.com/2018/07/07/walkthrough-creating-an-html-email-template-with-razor-and-razor-class-libraries-and-rendering-it-from-a-net-standard-class-library/
You have
var getViewResult = _viewEngine.GetView(executingFilePath: null, viewPath: viewName, isMainPage: true);
where executingFilePath is null.
Add executingFilePath so it leads to the view location on disk.
In my solution I have:
var getViewResult = _viewEngine.GetView(executingFilePath: executingFilePath, viewPath: viewName, isMainPage: true);
where executingFilePath is passed to RenderViewToStringAsync as additional parameter:
public class MessageBodyBuilderService : IMessageBodyBuilderService
{
private readonly IRazorViewToStringRenderer _razorViewToStringRenderer;
private readonly IWebHostEnvironment _hostingEnv;
private readonly string _pathToEmailTemplates = $"/Views/EmailTemplates/";
public MessageBodyBuilderService(
IWebHostEnvironment hostingEnv,
IRazorViewToStringRenderer razorViewToStringRenderer)
{
_hostingEnv = hostingEnv;
_razorViewToStringRenderer = razorViewToStringRenderer;
}
public async Task<BodyBuilder> BuildMessage<T>(string templateName, T modelForReplacement, bool isHtml = true)
{
string viewName = $"{_pathToEmailTemplates}{templateName}.cshtml";
string body = await _razorViewToStringRenderer.RenderViewToStringAsync(viewName, modelForReplacement, _hostingEnv.ContentRootPath);
var builder = new BodyBuilder()
{
HtmlBody = body
};
return builder;
}
}
where _hostingEnv.ContentRootPath comes from the ContentRootPath I declared on Startup:
AppDomain.CurrentDomain.SetData("ContentRootPath", webHostEnvironment.ContentRootPath);
and then you can pass executingFilePath (in your RazorViewToStringRenderer's RenderViewToStringAsync method) to FindView as additional parameter:
var view = FindView(executingFilePath, actionContext, viewName);
I hope it helps.

Related

How to create a Net Core Middleware to validate a route parameter

Using Net Core 7 I have Razor Pages with a culture route parameter:
#page "/{culture:validculture}/about"
I created a custom route constraint to check if culture has a valid value.
When the culture value is invalid I am redirected to a 404 error page.
public class CultureRouteConstraint : IRouteConstraint {
private readonly RequestLocalizationOptions _options;
public CultureRouteConstraint(IOptionsSnapshot<RequestLocalizationOptions> options) {
_options = options.Value;
}
public Boolean Match(HttpContext? httpContext, IRouter? route, String routeKey, RouteValueDictionary values, RouteDirection routeDirection) {
String? culture = Convert.ToString(value, CultureInfo.InvariantCulture);
List<String>? cultures = _options.SupportedCultures?.Select(x => x.TwoLetterISOLanguageName).ToList();
if (culture != null && cultures != null)
return cultures.Contains(culture, StringComparer.InvariantCultureIgnoreCase);
}
}
This works great if I hardcode de valid cultures, e.g:
List<String>? cultures = new() { "en", "pt" };
But if I inject RequestLocalizationOptions I get the error:
RouteCreationException: An error occurred while trying to create an instance of 'CultureRouteConstraint'.
Maybe I need to use a Middleware for this? How can I do this?
Just because IOptionsSnapshot Is registered as Scoped and therefore can't be injected into a Singleton service.
You could try with IOptions (Not Options<>)instead If you don't have to update the options for different request),For Example,I tried as below and it works well in my case:
In program.cs:
builder.Services.Configure<RequestLocalizationOptions>(opts =>
{
var supportedcultures = new List<CultureInfo>()
{
new CultureInfo("en-US"),
new CultureInfo("zh-CN")
};
opts.SupportedCultures = supportedcultures;
opts.SupportedUICultures= supportedcultures;
});
builder.Services.AddRazorPages();
builder.Services.AddRouting(options =>
options.ConstraintMap.Add("validculture", typeof(CultureRouteConstraint)));
var app = builder.Build();
The constraint:
public class CultureRouteConstraint : IRouteConstraint
{
private readonly RequestLocalizationOptions _options;
public CultureRouteConstraint(IOptions<RequestLocalizationOptions> options)
{
_options = options.Value;
}
public Boolean Match(HttpContext? httpContext, IRouter? route, String routeKey, RouteValueDictionary values, RouteDirection routeDirection)
{
String? culture = Convert.ToString(values["culture"], CultureInfo.InvariantCulture);
List<String>? cultures = _options.SupportedCultures?.Select(x => x.TwoLetterISOLanguageName).ToList();
if (culture != null && cultures != null)
return cultures.Contains(culture, StringComparer.InvariantCultureIgnoreCase);
else
return false;
}
}
The Result:
You could also check the document related
Looks like you are using DI to inject the RequestLocalizationOptions into your CultureRouteConstraint class. However, the IRouteConstraint interface does not support DI, so you cannot inject dependencies into it directly.
One option you could consider is using a middleware to handle the routing constraint. The middleware could use DI to inject the RequestLocalizationOptions and then check the culture value before passing control to the appropriate Razor page.
Here's an example of what this might look like:
public class CultureMiddleware
{
private readonly RequestDelegate _next;
private readonly RequestLocalizationOptions _options;
public CultureMiddleware(RequestDelegate next, IOptionsSnapshot<RequestLocalizationOptions> options)
{
_next = next;
_options = options.Value;
}
public async Task InvokeAsync(HttpContext context)
{
// Check the culture value
string culture = context.Request.RouteValues["culture"].ToString();
if (!_options.SupportedCultures.Select(x => x.TwoLetterISOLanguageName).Contains(culture, StringComparer.InvariantCultureIgnoreCase))
{
// Redirect to a 404 page if the culture value is invalid
context.Response.StatusCode = 404;
return;
}
// If the culture value is valid, pass control to the next middleware
await _next(context);
}
}
Seems like you cannot use a DI constructor when working with implementations of IRouteConstraint, but you have the HttpContext in your Match method. So, you can resolve the IOptionsSnapshot from there. This should work:
var opts = ctx
.RequestServices
.GetService<IOptionsSnapshot<RequestLocalizationOptions>>();

How to inject a service in my DbContext class and have host.MigrateDatabase() still working

I've got a working EFCore, .NET5, Blazor WASM application.
I call await host.MigrateDatabase(); in my Program.Main() to have my database always up-to-date.
public static async Task<IHost> MigrateDatabase(this IHost host)
{
using var scope = host.Services.CreateScope();
try
{
// Get the needed context factory using DI:
var contextFactory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<AppDbContext>>();
// Create the context from the factory:
await using var context = contextFactory.CreateDbContext();
// Migrate the database:
await context.Database.MigrateAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw;
}
return host;
}
In my AppDbContext I've overridden SaveChangesAsync() to add and update CreatedOn en UpdatedOn.
I mentioned this in DbContext.SaveChanges overrides behaves unexpected before.
I also want to fill CreatedBy and UpdatedBy with the userId.
I have an IdentityOptions class to hold the user data:
public class IdentityOptions
{
public string UserId => User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
public ClaimsPrincipal User { get; set; }
}
I've registered this class in StartUp like this:
services.AddScoped(sp =>
{
var context = sp.GetService<IHttpContextAccessor>()?.HttpContext;
var identityOptions = new IdentityOptions();
if (context?.User.Identity != null && context.User.Identity.IsAuthenticated)
{
identityOptions.User = context.User;
}
return identityOptions;
});
I inject this IdentityOptions class into several other services, without any problem.
But when I inject it in my AppDbContext:
public AppDbContext(DbContextOptions<AppDbContext> options, IdentityOptions identityOptions)
: base(options)
{
...
}
I get an error in MigrateDatabase():
"Cannot resolve scoped service 'IdentityOptions' from root provider."
I've been trying numerous options I found googling but can't find a solution that works for me.
Please advice.
Update:
services.AddDbContextFactory<AppDbContext>(
options => options.UseSqlServer(Configuration.GetConnectionString("DbConnection"),
b => b.MigrationsAssembly("DataAccess"))
#if DEBUG
.LogTo(Console.WriteLine, new [] {RelationalEventId.CommandExecuted})
.EnableSensitiveDataLogging()
#endif
);
Thanks to the great help of #IvanStoev (again), I found the answer.
Adding lifetime: ServiceLifetime.Scoped to AddDbContextFactory in Startup solved my problem.
Now I can use my IdentityOptions class in SaveChanges and automatically update my Created* and Updated* properties.

Swagger listing IFormFile parameter as type "object"

I have a controller that requests a model containing an IFormFile as one of it's properties. For the request description, the Swagger UI (I'm using Swashbuckle and OpenApi 3.0 for .NET Core) lists the type of the file property as type object. Is there some way to make the Swagger UI denote the exact type and it's JSON representation to help the client?
The controller requesting the model looks as follows.
[HttpPost]
[Consumes("multipart/form-data")
public async Task<IActionResult> CreateSomethingAndUploadFile ([FromForm]RequestModel model)
{
// do something
}
And the model is defined as below:
public class AssetCreationModel
{
[Required}
public string Filename { get; set; }
[Required]
public IFormFile File { get; set; }
}
We've been exploring this issue today. If you add the following to your startup it will convert IFormFile to the correct type
services.AddSwaggerGen(c => {
c.SchemaRegistryOptions.CustomTypeMappings.Add(typeof(IFormFile), () => new Schema() { Type = "file", Format = "binary"});
});
Also see the following article on file upload in .net core
https://learn.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-2.1
This problem was already tackled in the following github issue/thread.
This improvement was already merged into Swashbuckle.AspNetCore master (as per 10/30/2018), but i don't expect that to be available as a package soon.
There are simple solutions if you only have a IFormFile as a parameter.
public async Task UploadFile(IFormFile filePayload){}
For simple case you can take a look at the following answer.
For complicated cases like container cases, you can take a look at the following answer.
internal class FormFileOperationFilter : IOperationFilter
{
private struct ContainerParameterData
{
public readonly ParameterDescriptor Parameter;
public readonly PropertyInfo Property;
public string FullName => $"{Parameter.Name}.{Property.Name}";
public string Name => Property.Name;
public ContainerParameterData(ParameterDescriptor parameter, PropertyInfo property)
{
Parameter = parameter;
Property = property;
}
}
private static readonly ImmutableArray<string> iFormFilePropertyNames =
typeof(IFormFile).GetTypeInfo().DeclaredProperties.Select(p => p.Name).ToImmutableArray();
public void Apply(Operation operation, OperationFilterContext context)
{
var parameters = operation.Parameters;
if (parameters == null)
return;
var #params = context.ApiDescription.ActionDescriptor.Parameters;
if (parameters.Count == #params.Count)
return;
var formFileParams =
(from parameter in #params
where parameter.ParameterType.IsAssignableFrom(typeof(IFormFile))
select parameter).ToArray();
var iFormFileType = typeof(IFormFile).GetTypeInfo();
var containerParams =
#params.Select(p => new KeyValuePair<ParameterDescriptor, PropertyInfo[]>(
p, p.ParameterType.GetProperties()))
.Where(pp => pp.Value.Any(p => iFormFileType.IsAssignableFrom(p.PropertyType)))
.SelectMany(p => p.Value.Select(pp => new ContainerParameterData(p.Key, pp)))
.ToImmutableArray();
if (!(formFileParams.Any() || containerParams.Any()))
return;
var consumes = operation.Consumes;
consumes.Clear();
consumes.Add("application/form-data");
if (!containerParams.Any())
{
var nonIFormFileProperties =
parameters.Where(p =>
!(iFormFilePropertyNames.Contains(p.Name)
&& string.Compare(p.In, "formData", StringComparison.OrdinalIgnoreCase) == 0))
.ToImmutableArray();
parameters.Clear();
foreach (var parameter in nonIFormFileProperties) parameters.Add(parameter);
foreach (var parameter in formFileParams)
{
parameters.Add(new NonBodyParameter
{
Name = parameter.Name,
//Required = , // TODO: find a way to determine
Type = "file"
});
}
}
else
{
var paramsToRemove = new List<IParameter>();
foreach (var parameter in containerParams)
{
var parameterFilter = parameter.Property.Name + ".";
paramsToRemove.AddRange(from p in parameters
where p.Name.StartsWith(parameterFilter)
select p);
}
paramsToRemove.ForEach(x => parameters.Remove(x));
foreach (var parameter in containerParams)
{
if (iFormFileType.IsAssignableFrom(parameter.Property.PropertyType))
{
var originalParameter = parameters.FirstOrDefault(param => param.Name == parameter.Name);
parameters.Remove(originalParameter);
parameters.Add(new NonBodyParameter
{
Name = parameter.Name,
Required = originalParameter.Required,
Type = "file",
In = "formData"
});
}
}
}
}
}
You need to look into how you can add some/an OperationFilter that is suitable for your case.

Web API will not use ISerializable implementation

I thought I had jumped through the necessary hoops to get my JsonMediaTypeFormatter working with custom ISerializable implementations, complete with passing unit tests. But I'm unable to get it to work when I pass in values via Swagger UI.
My key questions are:
What am I doing wrong with my unit test causing it to serialize/deserialize different from what Web API is doing?
What do I need to change to get this working with Web API's serializing/deserialization and Swagger/Swashbuckle?
Class being serialized: (Notice that serializing and then deserializing drops off the time component and only keeps the date component. The helps for testing/observing purposes.)
public class Pet : ISerializable
{
public DateTime Dob { get; set; }
public Pet()
{
Dob = DateTime.Parse("1500-12-25 07:59:59");
}
public Pet(SerializationInfo info, StreamingContext context)
{
Dob = DateTime.Parse(info.GetString("Dob"));
}
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("Dob", Dob.Date.ToString());
}
}
Web API Method: (always returns null)
public class TestController : ApiController
{
[Route("~/api/Pet")]
public string Get([FromUri] Pet data)
{
return data.Dob.ToString();
}
}
Passing Unit Test: (and serialization helpers from MSDN docs)
[TestFixture]
public class SerializationTests
{
[Test]
public void PetTest()
{
var date = new DateTime(2017, 1, 20, 5, 0, 0);
var foo = new Pet { Dob = date };
var jsonFormatter = new JsonMediaTypeFormatter { SerializerSettings = new JsonSerializerSettings { ContractResolver = new DefaultContractResolver { IgnoreSerializableInterface = false } } };
var serialized = SerializationHelpers.Serialize(jsonFormatter, foo);
Console.WriteLine(serialized);
var deserialized = SerializationHelpers.Deserialize<Pet>(jsonFormatter, serialized);
Assert.That(foo.Dob, Is.Not.EqualTo(date.Date));
Assert.That(deserialized.Dob, Is.EqualTo(date.Date));
}
}
public static class SerializationHelpers
{
public static string Serialize<T>(MediaTypeFormatter formatter, T value)
{
// Create a dummy HTTP Content.
Stream stream = new MemoryStream();
var content = new StreamContent(stream);
// Serialize the object.
formatter.WriteToStreamAsync(typeof(T), value, stream, content, null).Wait();
// Read the serialized string.
stream.Position = 0;
return content.ReadAsStringAsync().Result;
}
public static T Deserialize<T>(MediaTypeFormatter formatter, string str) where T : class
{
// Write the serialized string to a memory stream.
Stream stream = new MemoryStream();
StreamWriter writer = new StreamWriter(stream);
writer.Write(str);
writer.Flush();
stream.Position = 0;
// Deserialize to an object of type T
return formatter.ReadFromStreamAsync(typeof(T), stream, null, null).Result as T;
}
}
WebApiConfig.cs
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
config.Formatters.Clear();
var jsonFormatter = new JsonMediaTypeFormatter { SerializerSettings = new JsonSerializerSettings { ContractResolver = new DefaultContractResolver { IgnoreSerializableInterface = false } } };
config.Formatters.Add(jsonFormatter);
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
A few other notes:
When I run the passing unit test, the Console.WriteLine output is:
{"Dob":"1/20/2017 12:00:00 AM"}
which is exactly what I want/expect.
My Swagger UI looks like this using the default Swashbuckle settings from Nuget. Note that value of the date is what is set in the default constructor, showing that my ISerializable implementation is ignored.
NOTE:
I have changed the question to remove all generics from the picture. This problem is fundamentally about ISerializable implementations now and not about Generics.
WebAPI api does not know how to deserialize this generic object. I see a similar question here in SO but did not personally try/test it. Hope it helps: Generic Web Api method
Rather than having a generic method, you can create a generic controller. So your code above will look something like below.
public abstract class MyClass{ }
public class PersonDto: MyClass{}
public class TestController<T> : ApiController where T: MyClass
{
public string Get([FromUri] T data)
{
...
}
}

Update of navigation property in EF7 does not get saved

I have simple model:
public class Post
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { set; get; }
//non-important properties stripped, to focus on problem
public virtual Resource Resource { set; get; }
public virtual ICollection<Tag> Tags { set; get; }
}
public class Resource
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { set; get; }
[Url]
public string Url { set; get; }
}
and DbContext (I use ASP.NET identity in this project, if this is relevant):
public class DbContext : IdentityDbContext<User>
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
var postEntity = modelBuilder.Entity<Post>();
postEntity.Reference(p => p.Resource).InverseCollection(); //no navigation property on Resource side
postEntity.Collection(p => p.Tags).InverseReference(tag => tag.Post);
postEntity.ToTable("Posts");
var resourceEntity = modelBuilder.Entity<Resource>();
resourceEntity.ToTable("Resources");
var tagEntity = modelBuilder.Entity<Tag>();
tagEntity.Reference(t => t.Post).InverseCollection(p => p.Tags).Required(false);
tagEntity.ToTable("Tags");
}
}
After migration (SQL Server), database tables looks like expected - Post table has Foreign Key to ResourceId.
Creating Post's works fine, when I attach post.Resource (already created Resource).
My problem occurs when I want to replace post.Resource.
By replace, I mean selecting one of already existing Resources and assigning it to post.
var resource2 = Database.Resources.First(r=>r.Url == "xyz");
I have tried:
post.Resource = resource2; Database.Entry(post).State = EntityState.Modified;
Database.Entry(post).Property(p => p.Resource).CurrentValue = resource2;
post.Resource = null;
With different combinations of them also, but none of them works. After calling await SaveChangesAsync(); and looking up in database - there are no changes. How to perform the replace (update of foreign key) properly?
//Update 14.09.2015
Issue was caused by additional select, performed to update One-To-Many relationship. Full code:
var Database new DbContext();
var resource2 = Database.Resources.First(r=>r.Url == "xyz");
var oldAssignedTags = Database.Posts.Include(p=>p.Tags).FirstOrDefault(p => p.Id == post.Id).Tags;
var tags = newTags as List<Tag> ?? newTags.ToList();
//TagComparer is irrelevant here
var toRemove = oldAssignedTags.Except(tags, TagComparer);
var toAdd = tags.Except(oldAssignedTags, TagComparer);
foreach (var tag in toRemove)
{
Database.Entry(tag).State = EntityState.Deleted; //Database.Tags.Remove(tag);
}
foreach (var tag in toAdd)
{
tag.Post = post;
post.Tags.Add(tag);
}
post.Resource = resource2;
await Database.SaveChangesAsync();
I thought this may have something to do with Eager Loading, however I can't reproduce your issue with or without AspNet.Identity. Running the below code results in the Resource always being updated. Using EntityFramework 7.0.0-beta7.
Code
static void Main(string[] args)
{
Init();
WithEagerLoading();
CleanUp();
Init();
WithoutEagerLoading();
CleanUp();
}
private static void WithoutEagerLoading()
{
var db = new MyContext();
var post = db.Posts.First(); // no eager loading of child
post.Resource = db.Resources.First(p => p.Url == "http://trend.atqu.in");
db.SaveChanges();
Console.WriteLine($"2nd Resource.Id: {new MyContext().Posts.Include(p => p.Resource).First().Resource.Id}");
}
private static void WithEagerLoading()
{
var db = new MyContext();
var post = db.Posts
.Include(p => p.Resource) // eager loading
.First();
post.Resource = db.Resources.First(p => p.Url == "http://trend.atqu.in");
db.SaveChanges();
Console.WriteLine($"2nd Resource.Id: {new MyContext().Posts.Include(p => p.Resource).First().Resource.Id}");
}
private static void CleanUp()
{
var db = new MyContext();
db.Posts.RemoveRange(db.Posts);
db.Resources.RemoveRange(db.Resources);
db.SaveChanges();
}
private static void Init()
{
var db = new MyContext();
var resource = new Resource { Url = "http://atqu.in" };
var resource2 = new Resource { Url = "http://trend.atqu.in" };
var post = new Post { Resource = resource };
db.Add(post);
db.Add(resource);
db.Add(resource2);
db.SaveChanges();
db = new MyContext();
post = db.Posts.Include(p => p.Resource).First();
resource = db.Resources.First(p => p.Url == "http://trend.atqu.in");
Console.WriteLine($"1st Resource.Id: {post.Resource.Id}");
}
Result
1st Resource.Id: 0f4d222b-4184-4a4e-01d1-08d2bc9cea9b
2nd Resource.Id: 00ccae9c-f0da-43e6-01d2-08d2bc9cea9b
1st Resource.Id: 749f08f0-2426-4043-01d3-08d2bc9cea9b
2nd Resource.Id: 2e83b512-e8bd-4583-01d4-08d2bc9cea9b
Edit 16/9
The problem in the edited question's code is because you are instantiating Database after you have retrieved post. post is not attached to that instance of Database, so when you attach the Resource to it and call SaveChangesAsync it does nothing, because post at that time has noting to do with the Database you are saving against. That is why your workaround of selecting post post again (after the instantiation) causes it to be fixed - because then the instance of post is attached. If you don't want to select post again, you should use the same DbContext instance to do the work above that you used to originally retrieve post.