.net core GraphQL, GraphQL.SystemTextJson: Serialization and deserialization of 'System.Type' instances are not supported - asp.net-core

In a ASP.NET core 5 application, I use GraphQL with GraphQL.SystemTextJson.
When I attempt to return a result, I get s System.NotSupportedException saying "Serialization and deserialization of 'System.Type' instances are not supported and should be avoided since they can lead to security issues.".
I suspect something to be missing in the configuration of DocumentWriter.
It is configured like this in ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
...
services.AddScoped<IDocumentWriter, DocumentWriter>();
Any suggestion?
Update:
for completeness, as asked by #AndrewSilver, I report the whole code (adapted from https://www.red-gate.com/simple-talk/dotnet/net-development/building-and-consuming-graphql-api-in-asp-net-core-3-1/ and ported to .net core 5.0).
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "GraphQlExperiments", Version = "v1" });
});
services.AddScoped<IDocumentExecuter, DocumentExecuter>();
services.AddScoped<IDocumentWriter, DocumentWriter>();
services.AddScoped<AuthorService>();
services.AddScoped<AuthorRepository>();
services.AddScoped<AuthorQuery>();
services.AddScoped<AuthorType>();
services.AddScoped<BlogPostType>();
services.AddScoped<ISchema, GraphQLDemoSchema>();
services.AddControllers();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "GraphQlExperiments v1"));
}
// See: https://github.com/JosephWoodward/graphiql-dotnet
app.UseGraphiQl("/graphiql", "/api/v1/graphql");
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
public class Author
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class BlogPost
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public Author Author { get; set; }
}
public class AuthorType : ObjectGraphType<Author>
{
public AuthorType()
{
Name = "Author";
Field(_ => _.Id).Description("Author's Id.");
Field(_ => _.FirstName).Description("First name of the author");
Field(_ => _.LastName).Description("Last name of the author");
}
}
public class BlogPostType : ObjectGraphType<BlogPost>
{
public BlogPostType()
{
Name = "BlogPost";
Field(_ => _.Id, type:
typeof(IdGraphType)).Description("The Id of the Blog post.");
Field(_ => _.Title).Description("The title of the blog post.");
Field(_ => _.Content).Description("The content of the blog post.");
}
}
public class AuthorQuery : ObjectGraphType
{
public AuthorQuery(AuthorService authorService)
{
int id = 0;
Field<ListGraphType<AuthorType>>(
name: "authors",
resolve: context =>
{
return authorService.GetAllAuthors();
});
Field<AuthorType>(
name: "author",
arguments: new QueryArguments(new QueryArgument<IntGraphType> { Name = "id" }),
resolve: context =>
{
id = context.GetArgument<int>("id");
return authorService.GetAuthorById(id);
}
);
Field<ListGraphType<BlogPostType>>(
name: "blogs",
arguments: new QueryArguments(new QueryArgument<IntGraphType> { Name = "id" }),
resolve: context =>
{
return authorService.GetPostsByAuthor(id);
}
);
}
}
public class GraphQLQueryDTO
{
public string OperationName { get; set; }
public string NamedQuery { get; set; }
public string Query { get; set; }
public string Variables { get; set; }
}
public class GraphQLDemoSchema : Schema, ISchema
{
public GraphQLDemoSchema(IServiceProvider resolver) : base(resolver)
{
Query = resolver.GetService<AuthorQuery>();
}
}
public class AuthorService
{
private readonly AuthorRepository _authorRepository;
public AuthorService(AuthorRepository
authorRepository)
{
_authorRepository = authorRepository;
}
public List<Author> GetAllAuthors()
{
return _authorRepository.GetAllAuthors();
}
public Author GetAuthorById(int id)
{
return _authorRepository.GetAuthorById(id);
}
public List<BlogPost> GetPostsByAuthor(int id)
{
return _authorRepository.GetPostsByAuthor(id);
}
}
public class AuthorRepository
{
private readonly List<Author> authors = new List<Author>();
private readonly List<BlogPost> posts = new List<BlogPost>();
public AuthorRepository()
{
Author author1 = new Author
{
Id = 1,
FirstName = "Joydip",
LastName = "Kanjilal"
};
Author author2 = new Author
{
Id = 2,
FirstName = "Steve",
LastName = "Smith"
};
BlogPost csharp = new BlogPost
{
Id = 1,
Title = "Mastering C#",
Content = "This is a series of articles on C#.",
Author = author1
};
BlogPost java = new BlogPost
{
Id = 2,
Title = "Mastering Java",
Content = "This is a series of articles on Java",
Author = author1
};
posts.Add(csharp);
posts.Add(java);
authors.Add(author1);
authors.Add(author2);
}
public List<Author> GetAllAuthors()
{
return this.authors;
}
public Author GetAuthorById(int id)
{
return authors.Where(author => author.Id == id).FirstOrDefault<Author>();
}
public List<BlogPost> GetPostsByAuthor(int id)
{
return posts.Where(post => post.Author.Id == id).ToList<BlogPost>();
}
}
[Route("/api/v1/graphql")]
public class GraphQLController : Controller
{
private readonly ISchema _schema;
private readonly IDocumentExecuter _executer;
public GraphQLController(
ISchema schema,
IDocumentExecuter executer
)
{
_schema = schema;
_executer = executer;
}
[HttpPost]
public async Task<IActionResult> Post([FromBody] GraphQLQueryDTO query)
{
var result = await _executer.ExecuteAsync(_ =>
{
_.Schema = _schema;
_.Query = query.Query;
_.Inputs = query.Variables?.ToInputs();
});
if (result.Errors?.Count > 0)
{
return BadRequest();
}
return Ok(result.Data);
}
}
And this is a sample request that triggers the error:
query {
author (id: 1){
id
firstName
lastName
}
blogs
{
id
title
content
}
}

I solved creating a custom JsonConverter:
public class CustomJsonConverterForType : JsonConverter<Type>
{
public override Type Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options
)
{
// Caution: Deserialization of type instances like this
// is not recommended and should be avoided
// since it can lead to potential security issues.
// If you really want this supported (for instance if the JSON input is trusted):
// string assemblyQualifiedName = reader.GetString();
// return Type.GetType(assemblyQualifiedName);
throw new NotSupportedException();
}
public override void Write(
Utf8JsonWriter writer,
Type value,
JsonSerializerOptions options
)
{
string assemblyQualifiedName = value.AssemblyQualifiedName;
// Use this with caution, since you are disclosing type information.
writer.WriteStringValue(assemblyQualifiedName);
}
}
Then, in configureServices:
services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.WriteIndented = true;
options.JsonSerializerOptions.Converters.Add(new CustomJsonConverterForType());
});

Instead of using System.Text.Json.JsonSearializer Use NewtonSoft.JsonConvert.SearializeObject

I fixed that problem by using the snippet shown in the docs: https://graphql-dotnet.github.io/docs/migrations/migration3
[HttpPost]
public async Task<IActionResult> Post([FromBody] GraphQLQueryDTO query)
{
var result = await _executer.ExecuteAsync(_ =>
{
_.Schema = _schema;
_.Query = query.Query;
_.Inputs = query.Variables?.ToInputs();
});
/* ----------- Added this ---------------------------------*/
HttpContext.Response.ContentType = "application/json";
HttpContext.Response.StatusCode = 200; // OK
var writer = new GraphQL.SystemTextJson.DocumentWriter();
await writer.WriteAsync(HttpContext.Response.Body, result);*
/* ------------------------------------------------------*/
if (result.Errors?.Count > 0)
{
return BadRequest();
}
return Ok(result.Data);
}
}

In your startup.cs, in ConfigureServices
Add AddNewtonsoftJson() after AddControllers()
services.AddControllers().AddNewtonsoftJson();

Related

ASP.NET Core : Fluent Api relationships configuration

There are a lot of examples how to use Fluent API in the internet, but mostly shows how configure one relationship between two models. In my case I need 3 relationships between 2 models. How to configure relationships between models below with Fluent API?
public class Company
{
public int Id { get; set; }
public string Name { get; set; }
public int FinanceEstimateId { get; set; }
public Estimate FinanceEstimate { get; set; }
public int EnvironmentEstimateId { get; set; }
public Estimate EnvironmentEstimate { get; set; }
public int SelfEstimateId { get; set; }
public Estimate SelfEstimate { get; set; }
}
public class Estimate
{
public int Id { get; set; }
public string Name { get; set; } // like: bad, good, excellent
public float Value { get; set; } // like: 1,2,3
}
Maybe this points you in the right direction.
I would go for 2 configurations like:
public class CompanyConfiguration : IEntityTypeConfiguration<Company>
{
public void Configure(EntityTypeBuilder<Company> builder)
{
builder.ToTable("Companies");
builder
.HasOne(x => x.EnvironmentEstimate)
.WithMany()
.HasForeignKey(x => x.EnvironmentEstimateId)
.OnDelete(DeleteBehavior.NoAction);
builder
.HasOne(x => x.FinanceEstimate)
.WithMany()
.HasForeignKey(x => x.FinanceEstimateId)
.OnDelete(DeleteBehavior.NoAction);
builder
.HasOne(x => x.SelfEstimate)
.WithMany()
.HasForeignKey(x => x.SelfEstimateId)
.OnDelete(DeleteBehavior.NoAction);
}
}
public class EstimateConfiguration : IEntityTypeConfiguration<Estimate>
{
public void Configure(EntityTypeBuilder<Estimate> builder)
{
builder.ToTable("Estimates");
}
}
You need a DbContext:
public class MyDbContext : DbContext
{
public DbSet<Company> Companies { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(#"CONNECTIONSTRING");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// applies the configuration (those IEntityTypeConfiguration<T> things)
modelBuilder.ApplyConfigurationsFromAssembly(typeof(MyDbContext).Assembly);
}
}
I created a console application that demonstrates the usage
using var ctx = new MyDbContext();
await ctx.Database.EnsureDeletedAsync();
await ctx.Database.EnsureCreatedAsync();
var company1 = new Company
{
Name = "Name1",
EnvironmentEstimate = new Estimate { Name = "EnvironmentEstimate1", Value = 1 },
FinanceEstimate = new Estimate { Name = "FinanceEstimate1", Value = 2 },
SelfEstimate = new Estimate { Name = "SelfEstimate1", Value = 3 }
};
var company2 = new Company
{
Name = "Name2",
EnvironmentEstimate = new Estimate { Name = "EnvironmentEstimate2", Value = 4 },
FinanceEstimate = new Estimate { Name = "FinanceEstimate2", Value = 5 },
SelfEstimate = new Estimate { Name = "SelfEstimate2", Value = 6 }
};
await ctx.Companies.AddAsync(company1);
await ctx.Companies.AddAsync(company2);
await ctx.SaveChangesAsync();
var result = await ctx.Companies.ToListAsync();
Console.WriteLine("Done");

system.outofmemoryexception swashbuckle.aspnetcore

I am having this issue when I am dealing with Geometry datatypes when I change the property to string everything works like a charm. Below you may see that I used schema filter to remove Ignored data member , and document filter to remove anything related to nettopology.
Property Name = GeoPoly
Swagger Config Class
public static IServiceCollection AddSwaggerModule(this IServiceCollection services)
{
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v2", new OpenApiInfo { Title = "Test API", Version = "0.0.1" });
c.SchemaFilter<MySwaggerSchemaFilter>();
c.DocumentFilter<RemoveBogusDefinitionsDocumentFilter>();
c.ResolveConflictingActions(x => x.First());
});
return services;
}
public static IApplicationBuilder UseApplicationSwagger(this IApplicationBuilder app)
{
app.UseSwagger(c =>
{
c.RouteTemplate = "{documentName}/api-docs";
});
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/v2/api-docs", "Test API");
});
return app;
}
}
public class MySwaggerSchemaFilter : Swashbuckle.AspNetCore.SwaggerGen.ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (schema?.Properties == null)
{
return;
}
var ignoreDataMemberProperties = context.Type.GetProperties()
.Where(t => t.GetCustomAttribute<IgnoreDataMemberAttribute>() != null);
foreach (var ignoreDataMemberProperty in ignoreDataMemberProperties)
{
var propertyToHide = schema.Properties.Keys
.SingleOrDefault(x => x.ToLower() == ignoreDataMemberProperty.Name.ToLower());
if (propertyToHide != null)
{
schema.Properties.Remove(propertyToHide);
}
}
}
}
public class RemoveBogusDefinitionsDocumentFilter : Swashbuckle.AspNetCore.SwaggerGen.IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
swaggerDoc.Components.Schemas.Remove("Districts");
swaggerDoc.Components.Schemas.Remove("Geometry");
swaggerDoc.Components.Schemas.Remove("CoordinateSequenceFactory");
swaggerDoc.Components.Schemas.Remove("GeometryOverlay");
swaggerDoc.Components.Schemas.Remove("NtsGeometryServices");
swaggerDoc.Components.Schemas.Remove("CoordinateEqualityComparer");
swaggerDoc.Components.Schemas.Remove("NtsGeometryServices");
swaggerDoc.Components.Schemas.Remove("GeometryFactory");
swaggerDoc.Components.Schemas.Remove("OgcGeometryType");
swaggerDoc.Components.Schemas.Remove("Coordinate");
swaggerDoc.Components.Schemas.Remove("Point");
}
}
Entity Class
public class Districts : BaseEntity<long>
{
public string DistrictsDesc { get; set; }
public string DistrictsDescAr { get; set; }
[IgnoreDataMember]
[Column(TypeName = "geometry")]
public Geometry GeoPoly { get; set; }
public IList<Records> Records { get; set; } = new List<Records>();
public long? RegionsId { get; set; }
public Regions Regions { get; set; }
public long? CitiesId { get; set; }
public Cities Cities { get; set; }
}
Is there a way to stop swashbuckle gen from dealing with datatypes other than documents filter ?

How to implement growing object with a design pattern?

I want to design a response and design it by responses in my asp.net core application. The simlpe response is like following.
public class Response {
public string Status { get; set; } => "Ok";
public BaseReport BaseReport { get;set;}
}
if user sends extra parameters to my service, I want to change my response content dynamically.
public class ReportsController : ControllerBase
{
[HttpGet]
public ActionResult<Response> GetReport(bool isEmployee, bool isFinanace, bool isInformatinTech)
{
// if all parameters fals, return base report.
var report = baseReposrService.Get();
var response = new Response() { BaseReport = report };
if(isEmployee)
{
var ereport = employeeService.Get();
var response = new Response() {
BaseReport = report,
EmployeeReport = ereport
};
}
if(isFinanace)
{
var freport = financeService.Get();
var response = new Response() {
BaseReport = report,
EmployeeReport = freport
};
}
...
...
}
}
the response object is growing by query parameters.
So, is implementing the decorator pattern for this problem right way? Is there any best practice for these type problems?
I tried as below:
public class Response
{
public Response()
{
BaseReport = new BaseReport();
}
public string Status { get; set; } ="Ok";
public BaseReport BaseReport { get; set; }
}
public class BaseReport
{
public string report { get; set; }
public string reportcontent { get; set; }
}
public interface IResponseFactory
{
IResponseFactory Add(string key);
Response Create(string key);
}
public class ResponseFactory1 : IResponseFactory
{
private readonly Dictionary<string, Response> _responsedic = new Dictionary<string, Response>();
public ResponseFactory1()
{
this.Add("Employee").Add("Finanace").Add("InformatinTech");
}
public IResponseFactory Add(string key)
{
var response = new Response();
response.BaseReport.report = key;
response.BaseReport.reportcontent = key + "content";
_responsedic.Add(key, response);
return this;
}
public Response Create(string responsename)
{
if (_responsedic.ContainsKey(responsename))
{
return _responsedic[responsename];
}
else
{
return new Response() { BaseReport = new BaseReport() { report = "basereport",reportcontent= "basereportcontent" } };
}
}
in startup class:
services.AddSingleton<IResponseFactory, ResponseFactory1>();
in controller:
[HttpGet]
public ActionResult<Response> GetReport(string responsetype)
{
var response = _responseFactory.Create(responsetype);
return response;
}

ASP.Net Core storing and retrieving an entity against identity user

I am building a Web API and have implemented registration and login. I have a model called Task which is as following:
public class User_Task
{
[Key]
public long TaskId { get; set; }
public string What { get; set; }
public string How_often { get; set; }
public string How_important { get; set; }
[ForeignKey("FeatureId")]
public long? FeatureId { get; set; }
public virtual ICollection<Step> Steps { get; set; }
public User_Task()
{
}
}
It's repository:
public class User_TaskRepository : IUser_TaskRepository
{
private readonly WebAPIDataContext _context;
public User_TaskRepository(WebAPIDataContext context)
{
_context = context;
}
public IEnumerable<User_Task> GetAll()
{
return _context.User_Tasks.Include(task => task.Steps).ToList();
}
public void Add(User_Task item)
{
_context.User_Tasks.Add(item);
_context.SaveChanges();
}
public User_Task Find(long key)
{
return _context.User_Tasks.Include(task => task.Steps).FirstOrDefault(t => t.TaskId == key);
}
public void Remove(long key)
{
var entity = _context.User_Tasks.First(t => t.TaskId == key);
_context.User_Tasks.Remove(entity);
_context.SaveChanges();
}
public void Update(User_Task item)
{
_context.User_Tasks.Update(item);
_context.SaveChanges();
}
}
public interface IUser_TaskRepository
{
void Add(User_Task item);
IEnumerable<User_Task> GetAll();
User_Task Find(long key);
void Remove(long key);
void Update(User_Task item);
}
And it's controller:
[Route("api/[controller]")]
public class User_TaskController : Controller
{
private readonly IUser_TaskRepository _taskRepository;
//Controller
public User_TaskController(IUser_TaskRepository taskRepository)
{
_taskRepository = taskRepository;
}
//Get methods
[HttpGet]
public IEnumerable<User_Task> GetAll()
{
return _taskRepository.GetAll();
}
[HttpGet("{id}", Name = "GetTask")]
public IActionResult GetById(long id)
{
var item = _taskRepository.Find(id);
if (item == null)
{
return NotFound();
}
return new ObjectResult(item);
}
//Create
[HttpPost]
public IActionResult Create([FromBody] User_Task item)
{
if (item == null)
{
return BadRequest();
}
_taskRepository.Add(item);
return CreatedAtRoute("GetTask", new { id = item.TaskId }, item);
}
//Update
[HttpPut("{id}")]
public IActionResult Update(long id, [FromBody] User_Task item)
{
if (item == null)
{
return BadRequest();
}
var task = _taskRepository.Find(id);
if (task == null)
{
return NotFound();
}
task.What = item.What;
task.How_often = item.How_often;
task.How_important = item.How_important;
UpdateTaskSteps(item.Steps, task.Steps);
_taskRepository.Update(task);
return new NoContentResult();
}
private void UpdateTaskSteps(ICollection<Step> steps, ICollection<Step> taskSteps)
{
foreach (var step in steps)
{
Step taskStep = taskSteps.FirstOrDefault(x => x.StepId == step.StepId);
if (taskStep != null)
{
// Update
taskStep.What = step.What;
}
else
{
// Create
taskSteps.Add(new Step
{
What = step.What,
TaskId = step.TaskId
});
}
}
}
//Delete
[HttpDelete("{id}")]
public IActionResult Delete(long id)
{
var task = _taskRepository.Find(id);
if (task == null)
{
return NotFound();
}
_taskRepository.Remove(id);
return new NoContentResult();
}
}
Now I have ApplicationUser model as following:
public class ApplicationUser : IdentityUser
{
// Extended Properties
public string FirstName { get; set; }
public string LastName { get; set; }
public ApplicationUser()
{
}
}
And yet another Stakeholder model:
public class Stakeholder
{
public int Id { get; set; }
public string IdentityId { get; set; }
public ApplicationUser Identity { get; set; } // navigation property
public Stakeholder()
{
}
}
How can I make sure that each Task is created against the logged in user i.e. Stakeholder? I will have to update my Task model with a foreign key to Stakeholder? How can I do that, and how can update my controller methods so that I can send back Tasks belonging to the user/Stakeholder making the request?
UPDATE: startup.cs
public class Startup
{
private const string SecretKey = "iNivDmHLpUA223sqsfhqGbMRdRj1PVkH"; // todo: get this from somewhere secure
private readonly SymmetricSecurityKey _signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(SecretKey));
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddDbContext<WebAPIDataContext>(options =>
{
options.UseMySql(Configuration.GetConnectionString("MysqlConnection"),
b => b.MigrationsAssembly("Vision_backlog_backend"));
});
services.AddSingleton<IJwtFactory, JwtFactory>();
// jwt wire up
// Get options from app settings
var jwtAppSettingOptions = Configuration.GetSection(nameof(JwtIssuerOptions));
// Configure JwtIssuerOptions
services.Configure<JwtIssuerOptions>(options =>
{
options.Issuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
options.Audience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)];
options.SigningCredentials = new SigningCredentials(_signingKey, SecurityAlgorithms.HmacSha256);
});
services.AddScoped<IProfileRepository, ProfileRepository>();
services.AddScoped<IUser_TaskRepository, User_TaskRepository>();
services.AddScoped<IFeatureRepository, FeatureRepository>();
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
});
// api user claim policy
services.AddAuthorization(options =>
{
options.AddPolicy("ApiUser", policy => policy.RequireClaim(Constants.Strings.JwtClaimIdentifiers.Rol, Constants.Strings.JwtClaims.ApiAccess));
});
services.AddIdentity<ApplicationUser, IdentityRole>
(o =>
{
// configure identity options
o.Password.RequireDigit = false;
o.Password.RequireLowercase = false;
o.Password.RequireUppercase = false;
o.Password.RequireNonAlphanumeric = false;
o.Password.RequiredLength = 6;
})
.AddEntityFrameworkStores<WebAPIDataContext>()
.AddDefaultTokenProviders();
services.AddMvc().AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<Startup>());
services.AddAutoMapper();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info { Title = "My API", Version = "v1" });
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
// global policy - assign here or on each controller
app.UseCors("CorsPolicy");
var jwtAppSettingOptions = Configuration.GetSection(nameof(JwtIssuerOptions));
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)],
ValidateAudience = true,
ValidAudience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)],
ValidateIssuerSigningKey = true,
IssuerSigningKey = _signingKey,
RequireExpirationTime = false,
ValidateLifetime = false,
ClockSkew = TimeSpan.Zero
};
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
TokenValidationParameters = tokenValidationParameters
});
app.UseMvc();
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS etc.), specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
}
}
From what I've understood, you want each Stakeholder to have a list of User_Task.
I suggest you add a foreign key to your User_Task class which references the Stakeholder Id, then add navigation properties to your User_Task and Stakeholder classes.
The following should work:
User_Task class:
public class User_Task
{
[Key]
public long TaskId { get; set; }
public string What { get; set; }
public string How_often { get; set; }
public string How_important { get; set; }
[ForeignKey("FeatureId")]
public long? FeatureId { get; set; }
public virtual ICollection<Step> Steps { get; set; }
// EF should detect a reference to another table if your property name follows the {className}{idName} format
// so the ForeignKey attribute isn't really needed
[ForeignKey("StakeholderId")]
[Required]
public int StakeholderId { get; set; }
public Stakeholder Stakeholder { get; set; }
public User_Task()
{
}
}
Stakeholder class:
public class Stakeholder
{
public int Id { get; set; }
public string IdentityId { get; set; }
public ApplicationUser Identity { get; set; }
// navigation property for User_Tasks
public ICollection<User_Task> User_Tasks { get; set; }
public Stakeholder()
{
}
}
For your repository class, you could have a method that returns all Tasks that belong to a certain Stakeholder based on the logged in user's Id:
public ICollection<User_Task> GetUserTasks(string userId){
Stakeholder currentStakeholder = _context.Stakeholders
.FirstOrDefault(sh => sh.IdentityId == userId);
var userTasks = _context.User_Tasks
.Where(task => task.StakeholderId == currentStakeholder.Id).ToList();
return userTasks;
}
Now to get the logged in user's Id, you have to use the UserManager class, which should be injected into your DI Container by IdentityServer if you've set it up correctly. So you just have to add a UserManager to your controller's constructor.
The Controller class has a property called "User", which you can pass to the GetUserId() method of the UserManager class:
[Route("api/[controller]")]
public class User_TaskController : Controller
{
private readonly IUser_TaskRepository _taskRepository;
private readonly UserManager<ApplicationUser> _userManager;
//Controller
public User_TaskController(IUser_TaskRepository taskRepository, UserManager<ApplicationUser> userManager)
{
_taskRepository = taskRepository;
_userManager = userManager;
}
// The Authorize header means that this method cannot be accessed if the requester is not authenticated
[Authorize]
[HttpGet("current")]
public IActionResult GetCurrentUserTasks()
{
string currentUserId = _userManager.GetUserId(User);
var userTasks = _taskRepository.GetUserTasks(userId);
return userTasks;
}
}
Some additional things to consider:
You might want to adopt RESTful style when it comes to your APIs. Consider making the logged in user access his own tasks through another controller that follows a pattern like: /Account/Tasks
Since EF Core does not support Lazy Loading yet, you don't need to add the "virtual" keyword before navigation properties
You can also setup foreign keys in your DbContext's OnModelCreating method as follows:
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<User_Task>().HasOne(t => t.Stakeholder).WithMany(sh => sh.User_Tasks).HasForeignKey(t => t.StakeholderId);
}
Update
Adding a Task to a specific user in your repository class:
public void Add(string userId, User_Task item)
{
Stakeholder currentStakeholder = _context.Stakeholders
.FirstOrDefault(sh => sh.IdentityId == userId);
item.StakeholderId = currentStakeholder.Id;
_context.User_Tasks.Add(item);
_context.SaveChanges();
}
You could also add a Task to a Stakeholder by calling "Add()" to a Stakeholder object's User_Tasks ICollection.
Another thing to keep in mind: You should probably use DTOs when dealing with input for creating your entities. Users shouldn't have the possibility of setting the primary keys of entries, unless that's something you want because of some use case.

RavenDB UniqueConstraint doesn't seem to work

I've been trying for a day to get UniqueConstraint working, but it doesn't seem the are. I have a simple MVC6 site that creates a User on a POST. I'm expecting that on the second POST an exception should be thrown as a user will have already been created with the same properties. I'm wanting to ensure that the email address is unique.
using Raven.Client;
using Raven.Client.Document;
using Raven.Client.UniqueConstraints;
namespace MVC6Test.DomainModel
{
public class User
{
public string Id { get; private set; }
[UniqueConstraint]
public string Email { get; set; }
public string Password { get; set; }
public string Name { get; set; }
}
}
namespace MVC6Test.Web.Controllers
{
public class AdminController : Microsoft.AspNet.Mvc.Controller
{
private IDocumentStore _documentStore { get; set; }
public IDocumentSession Session { get; set; }
[HttpPost]
[AllowAnonymous]
[Route("login")]
public async Task<IActionResult> Login(string userName, string password)
{
User user = new User() {
Email = "test#gmail.com"
};
Session.Store(user);
}
public override void OnActionExecuting(ActionExecutingContext context)
{
if (_documentStore.IsDefault()) {
_documentStore = context.HttpContext.RequestServices.GetRequiredService<IDocumentStore>();
}
Session = _documentStore.OpenSession();
base.OnActionExecuting(context);
}
public override void OnActionExecuted(ActionExecutedContext context)
{
using (Session) {
if (Session != null && context.Exception == null) {
Session.SaveChanges();
}
}
base.OnActionExecuted(context);
}
}
}
namespace MVC6Test.Web
{
public class Startup
{
private IDocumentStore DocumentStore;
public void ConfigureServices(IServiceCollection services)
{
DocumentStore = new DocumentStore {
DefaultDatabase = "MVC6Test",
Url = "http://localhost:3366"
};
DocumentStore.Listeners.RegisterListener(new UniqueConstraintsStoreListener());
DocumentStore.Initialize();
services.TryAddSingleton(typeof(IDocumentStore), (provider) => {
return DocumentStore;
});
}
public void Configure(IApplicationBuilder app, IApplicationLifetime lifetime)
{
lifetime.ApplicationStopped.Register(() => {
DocumentStore.Dispose();
});
}
}
}
I do get this metadata on the items that are created:
{
"Raven-Entity-Name": "Users",
"Raven-Clr-Type": "MVC6Test.DomainModel.User, MVC6Test",
"Ensure-Unique-Constraints": [
{
"Name": "Email",
"CaseInsensitive": false
}
]
}