how to make a column Unique based on comparing grouping id - c#

I need to make sure that all View data for a specific Site element is unique, but I want to be able to reuse the value for other Site's with different id's.
Example:
Site One
View one ref: ViewOne
View two ref: ViewOne <-- error
Site Two
View one ref: ViewOne <-- no error since it's a different site
View two ref: ViewTwo
View model:
public class View : AssetsBase, IView
{
public int SiteId { get; set; }
public string Name { get; set; }
public string Ref { get; set; }
public virtual IEnumerable<MetaEntry> MetaEntries { get; set; } = new HashSet<MetaEntry>();
public virtual IEnumerable<HreflangEntry> HreflangEntries { get; set; } = new HashSet<HreflangEntry>();
}
DB Context:
builder.Entity<View>().ToTable("SiteView").HasIndex(sw => sw.Ref).IsUnique();
the current approach works as expected, but I'm limited to only use a Ref value once. is what I want possible?

Thanks to user700390 who provided the solution.
DB Context:
builder.Entity<View>().ToTable("SiteView").HasIndex(sw => new { sw.Ref, sw.SiteId }).IsUnique();

Related

How do I use a database view as a model in C# MVC

I am using a DATABASE-FIRST approach in C# MVC and all of my generated models are in a sub-folder Models>Generated. One of these models is called SourceSystem which contains the field definitions of the table and the related table entities.
public partial class SourceSystem
{
[Key]
[Column("ID")]
public int Id { get; set; }
[StringLength(100)]
public string SystemName { get; set; } = null!;
[Column("ROWSTAMP")]
public byte[] Rowstamp { get; set; } = null!;
[StringLength(100)]
public string? LinkedServerName { get; set; }
[StringLength(100)]
public string? DatabaseName { get; set; }
[StringLength(20)]
public string? DefaultSourceSchema { get; set; }
[StringLength(20)]
public string? DefaultTargetSchema { get; set; }
[InverseProperty("SourceSystem")]
public virtual ICollection<Domain> Domains { get; } = new List<Domain>();
[InverseProperty("SourceSystem")]
public virtual ICollection<EventProfile> EventProfiles { get; } = new List<EventProfile>();
}
As part of the application there are also a number of synonyms created which will link back to the source databases (based on the Linked Server Name, Database Name and Default Source Schema. This list of synonymns does not live in MY database but are in the msdb database so I have a view that enables me to generate a dataset of the synonyms and associate them back to the SourceSystem table. For note, DelimitedSpit8K takes a string and spits it up, into a record set. Because synonyms use a 2/3/4 part naming convention, I have to reverse them as I need to definately have the last two parts (schema and object name) but the first two (linked server name and database) are optional. Note also that the schema for the view is pow, not the default dbo.
CREATE VIEW pow.Synonymn AS
SELECT
SYN.object_id AS [SystemID]
,SYN.name AS [Synonym]
,SCH.name AS [SourceSchema]
,SYN.base_object_name
,REPLACE(REPLACE(REVERSE(object_name.Item),'[',''),']','') AS [object_name]
,REPLACE(REPLACE(REVERSE(object_schema.Item),'[',''),']','') AS [object_schema]
,REPLACE(REPLACE(REVERSE(object_db.Item),'[',''),']','') AS [object_db]
,REPLACE(REPLACE(REVERSE(object_linked_server.Item),'[',''),']','') AS [object_linked_server]
,SS.ID AS [SourceSystem_Id]
FROM
sys.synonyms AS SYN
JOIN
sys.schemas AS SCH ON SCH.schema_id = SYN.schema_id
JOIN
pow.SourceSystem AS SS ON SS.DefaultTargetSchema = SCH.name
CROSS APPLY
pow.DelimitedSplit8K(REVERSE(SYN.base_object_name), '.') AS [object_name]
CROSS APPLY
pow.DelimitedSplit8K(REVERSE(SYN.base_object_name), '.') AS [object_schema]
CROSS APPLY
pow.DelimitedSplit8K(REVERSE(SYN.base_object_name), '.') AS [object_db]
CROSS APPLY
pow.DelimitedSplit8K(REVERSE(SYN.base_object_name), '.') AS [object_linked_server]
WHERE
object_name.ItemNumber =1
AND
object_schema.ItemNumber = 2
AND
object_db.ItemNumber = 3
AND
(
object_linked_server.ItemNumber IS NULL
OR
object_linked_server.ItemNumber = 4
)
I have manually added a model to my models folder (not Models>Generated):
using Overwatch_API.Models.Generated;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using Microsoft.EntityFrameworkCore;
namespace Overwatch_API.Models;
//[Table("Synonym", Schema = "pow")]
public partial class Synonym
{
[Key]
[Column("SystemID")]
public int SystemID { get; set; }
[Column("Synonym")]
public string SynonymName { get; set; }
[Column("SourceSystemTargetSchema")]
public string SourceSchema { get; set; } = null!;
[Column("SourceSystemId")]
public int SourceSystem_Id { get; set; }
public string base_object_name { get; set; }
public string object_name { get; set; }
public string object_schema { get; set; }
public string object_db { get; set; }
public string object_linked_server { get; set; }
[ForeignKey("SourceSystemId")]
[InverseProperty("Synonyms")]
//[JsonIgnore]
public virtual SourceSystem SourceSystem { get; set; } = null!;
}
and I have modified the database context:
using Overwatch_API.Models;
...
public virtual DbSet<Synonym> Synonyms { get; set; }
...
modelBuilder.Entity<Synonym>(entity =>
{
entity.ToView(nameof(Synonym))
.HasKey(t => t.SystemID);
});
and I have updated the ViewModel for the SourceSystemVM:
using Overwatch_API.Models;
...
public class SourceSystemVM
{
public int Id { get; set; }
[DisplayName("System Name")]
public string SystemName { get; set; }
[DisplayName("Linked Server")]
public string? LinkedServerName { get; set; }
[DisplayName("Linked Database")]
public string? DatabaseName { get; set; }
[DisplayName("Source Schema")]
public string? DefaultSourceSchema { get; set; }
[DisplayName("Target Schema")]
public string? DefaultTargetSchema { get; set; }
public ICollection<DomainVM> domains { get; set; }
public ICollection<Synonym> synonyms { get; set; }
public SourceSystemVM(SourceSystem ss)
{
Id = ss.Id;
SystemName = ss.SystemName;
LinkedServerName = ss.LinkedServerName;
DatabaseName = ss.DatabaseName;
DefaultSourceSchema = ss.DefaultSourceSchema;
DefaultTargetSchema = ss.DefaultTargetSchema;
domains = new List<DomainVM>();
synonyms = new List<Synonym>();
}
}
When I start the server and run Swagger, and choose the api endpoint
https://localhost:7001/api/SourceSystems
I get the following error message:
InvalidOperationException: The [InverseProperty] attribute on property 'Synonym.SourceSystem' is not valid. The property 'Synonyms' is not a valid navigation on the related type 'SourceSystem'. Ensure that the property exists and is a valid reference or collection navigation.
I am not sure what part of the configuration I have got wrong. I don't want to touch the SourceSytem.cs in the Models>Generated folder as it will get overwritten if the DF models are re-generated. Do I need to create a new partial class in the models folder to extend the generated model, and if so, what would that look like and how do I disambiguate between the Models>SourceSystem.cs and the Models>Generated>SourceSystem.cs when referencing it in the VM and DTOs. Or am I missing an entire concept somewhere?
For context, the Synonym collection is used for view (read) only. The functionality to add a new synonym will have to be managed through a call to a SQL stored procedure, but I need to understand what I have screwed up here first :)
UPDATE
I have added the partial class to the Models folder:
using Overwatch_API.Models;
using Overwatch_API.Models.Generated;
using System.ComponentModel.DataAnnotations.Schema;
namespace Overwatch_API.Models.Generated
{
public partial class SourceSystem
{
[InverseProperty("SourceSystem")]
public virtual ICollection<Synonym> Synonyms { get; } = new List<Synonym>();
}
}
and updated the context:
modelBuilder.Entity<Synonym>(entity =>
{
entity.ToView("Synonym", "pow");
});
and now the API doesn't throw an error message but the synonym array is empty and I'm not sure why: Whether the relationship between the Synonym and SourceSystems is not defined correctly or if the view is not being found/executed to return the details.
UPDATE 2: As per the question from Alex:
I have set up the following in the dbContext:
modelBuilder.Entity<Synonym>(entity =>
{
entity.ToView("Synonym", "pow");
entity.HasOne(d => d.SourceSystem)
.WithMany(p => p.Synonyms)
.HasForeignKey(x => x.SourceSystemId);
});
The Profiler is showing the following queries being run. For the Synonymns API [HttpGet]
SELECT [s].[SystemID], [s].[SourceSchema], [s].[SourceSystem_ID], [s].[Synonym], [s].[base_object_name], [s].[object_db], [s].[object_linked_server], [s].[object_name], [s].[object_schema]
FROM [pow].[Synonym] AS [s]
For the SourceSystem API [HttpGet]
SELECT [s].[ID], [s].[DatabaseName], [s].[DefaultSourceSchema], [s].[DefaultTargetSchema], [s].[LinkedServerName], [s].[ROWSTAMP], [s].[SystemName], [d].[ID], [d].[DomainName], [d].[ROWSTAMP], [d].[SourceSystem_ID]
FROM [pow].[SourceSystem] AS [s]
LEFT JOIN [pow].[Domain] AS [d] ON [s].[ID] = [d].[SourceSystem_ID]
ORDER BY [s].[ID]
Domain is another collection within SourceSystem but it unrelated to the Synonyms. A single join here would create a cartesion collection with both the Domains and Synonymns being repeated. Could this be the problem? The data fetch would either need to do an N+1 query or bring back the cartesian collection and then filter distinct. If so, how do I get around the problem. Is there a way to lazy-load the synonymns in MVC. I could just load them all in the front end (React/Next) and apply a filter in JS to only show the ones connected with the selected SourceSystem but this is spreading the logic about throughout the application stack.
OK. So I worked it out and it is non-trivial so hoping that this helps someone else.
SourceSystems contains multiple ICollections (Domains and Synonyms amongst them) however these create cyclic dependencies so the get uses a SourceSystemViewModel which uses a DomainViewModel which does not contain the cyclic reference back to the SourceSystem. I had to add the Synonyms to the SourceSystemViewModel, but as the Synonyms also contain the cyclic reference I have to create a SynonymViewModel as well.
and then in the SourceSystemsController, when executing the _context.SourceSystems you have to tell it to .Include("ChildCollection") which I had not done.
var ssList = _context.SourceSystems
.Include("Domains")
.Include("Synonyms")
.ToList();
Once this is included you then have to specifically iterate through the ssList, and for each SourceSystemDTO, iterate through both the Domains list and the Synonyms list and map the list items into the relevant arrays.
[HttpGet]
[ResponseType(typeof(List<SourceSystemVM>))]
public async Task<IEnumerable<SourceSystemVM>> GetSourceSystems()
{
List<SourceSystem> ss = _context.SourceSystems.ToList();
IEnumerable<SourceSystemVM> ssDTO = from s in ss select new SourceSystemVM(s);
var ssList = _context.SourceSystems
.Include("Domains")
.Include("Synonyms")
.ToList();
var ssDTOList = new List<SourceSystemVM>();
ssList.ForEach(ss =>
{
var ssDTO = new SourceSystemVM(ss);
foreach (var domain in ss.Domains)
{
var domainDTO = new DomainVM(domain);
ssDTO.domains.Add(domainDTO);
}
foreach (var synonym in ss.Synonyms)
{
var synonymDTO = new SynonymVM(synonym);
ssDTO.synonyms.Add(synonymDTO);
}
ssDTOList.Add(ssDTO);
});
return ssDTOList;
}

EF Core NullReferenceException on Related Navigation Property

I have two related models.
public class Offer
{
public long Id { get; set; }
public string OfferCode { get; set; }
public string Description { get; set; }
// more properties
public int ProductId { get; set; }
public virtual Product Product { get; set; }
}
public class Product
{
public long Id { get; set; }
public string Name { get; set; }
// more properties
public virtual ICollection<Offer> Offers { get; set; }
}
I am trying to have an MVC form with a select HTML element where Offers are grouped and products
and have the Product Names serve as optgroups.
To this end, I have a view model that I intend to populate with the grouped Offers and I have a method
to do just that.
private OfferMessageViewModel PrepareViewModel(OfferMessageViewModel viewModel)
{
var offers = _context.Offers.Include(o => o.Product).ToList()
.GroupBy(o => o.Product.Name).ToList();
foreach (var offerGroup in offers)
{
var optionGroup = new SelectListGroup
{
Name = offerGroup.Key
};
foreach (var offer in offerGroup)
{
viewModel.Offers.Add(
new SelectListItem
{
Value = offer.OfferCode,
Text = offer.Description,
Group = optionGroup
}
);
}
}
return viewModel;
}
The code gets tripped up in the GroupBy clause.
o.Product is null even when o.ProductID has a value in it.
The ToList() call right before the GroupBy is not helping.
I have tried removing the virtual modifiers on the related entities
navigation properties but the error persisted.
Installing the NuGet package Microsoft.EntityFrameworkCore.Proxies and
modifying and configuring it as such
services.AddDbContext<ApplicationDbContext>(options =>
options.UseLazyLoadingProxies()
.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
also did not make the error go away.
Is there something else I am missing?
Any help would be greatly appreciated.
EDIT:
It has been suggested that my post might be solved by this SO question. But I get the null reference exception even with lazy loading explicitly turned on.
I have tried the suggested solutions there but still no luck.
I eventually solved it.
Apparently the problem was that the foreign key was an int referencing a primary key of type long.
So I changed
public int ProductId { get; set; }
to
public long ProductId { get; set; }
in the Offer model.
Added the necessary migration, updated the database and now it works.
No more null reference exceptions.
Don't know why I missed that but it's probably a combination of lack of sleep and
a not-so-helpful error message throwing me off in a completely different direction.

Web Api, Update DB, connection reset 200ok

I have asp.net web api application. I have the table Companies in the databse which have two fields: id and description. Recently I've updated the database and added a new column called CustomerID. After that when I am trying to call getCompanies
private readonly BackendContext _context;
public CompaniesController(BackendContext context)
{
_context = context;
}
// GET: api/Companies
[HttpGet]
public IEnumerable<Company> GetCompanies()
{
return _context.Companies;
}
I get
I think the controller tries to return the old companies model but can't achieve it because it doesnt exist now but I don't know how to fix this though the controller should return the updated model. Maybe I should somehow rebuild the app to make it use the updated version?
Additional code:
Context
public class BackendContext : Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityDbContext<IdentityUser>//DbContext
{
public BackendContext(DbContextOptions<BackendContext> options) : base(options) { }
public DbSet<Company> Companies { get; set; }
public DbSet<CompanyToProduct> CompanyToProducts { get; set; }
public DbSet<Product> Products { get; set; }
public DbSet<Customer> Customers { get; set; }
public DbSet<Vendor> Vendors { get; set; }
public DbSet<VendorToProduct> VendorToProducts { get; set; }
public DbSet<Invoice> Invoices { get; set; }
public DbSet<InvoiceItem> InvoiceItems { get; set; }
}
Model
public class Company
{
public int ID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public int CustomerID { get; set; }
public virtual Customer Customer { get; set; }
public virtual ICollection<CompanyToProduct> CompaniesToProducts { get; set; }
public virtual ICollection<Invoice> Invoices { get; set; }
}
UPDATE
I've added some values to the table and I got the response of the first company:
[{"id":1,"name":"Google","description":"free food","customerID":6,"customer":null,"companiesToProducts":null,"invoices":null}
BUT I also got the fields which is not specified in the table: customer, companiesToProducts,invoices. Invoices and companiesToProducts are tables in my database and I don't know what is customer referred to. I should also mention that these tables are connected by foreign key.
UPDATE
Error:
Based on the comments on the question above, it sounds like the related tables are all trying to serialize and the overall process is failing likely due to circular references in the object graph. This comment above in particular hints at a solution:
I want to return only the data about companies but the controller also returns another fields like customer, companiesToProducts,invoices
While it's convenient to just return directly from the data context, this has the added side-effect of coupling the API with the database (and with the data access framework, which appears to be the issue here). In API design in general it's always a good idea to explicitly define the "shape" of that API. The fields to return, etc.
Project your result into an explicitly defined shape and return only what you want to return:
var result = _context.Companies
.Select(c => new
{
c.ID,
c.Name,
c.Description,
c.CustomerID
})
.ToList();
This defines specifically what you want to return, fetches only that information from the backing data, materializes it into an in-memory list, and finally then returns it through the API.
There is a potential downside to this, however. Because now we also need to change the return type of your API method. There are a couple options there, such as returning a generic response object or creating a view model which closely approximates your already existing model and starts to feel like duplication.
As with just about anything, it's a balance. Too far in any one direction and that direction starts to become a problem. Personally I often go the route of defining a view model to return:
public class CompanyViewModel
{
public int ID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public int CustomerID { get; set; }
}
and returning that:
return _context.Companies
.Select(c => new CompanyViewModel
{
ID = c.ID,
Name = c.Name,
Description = c.Description,
CustomID = c.CustomerID
})
.ToList();
But the reason I normally do this is because I normally work in an environment where the web application is just one application attached to a common shared business domain, so the view models don't feel like code duplication. They're in a separate project, often take a different shape than the backing data objects, etc. But if your domain models are already in your web project and that's the only project you have, there's a strong desire to want to return those.
Another option when that's the case could be to universally set your JSON serialization to ignore circular references:
services.AddMvc()
.AddJsonOptions(
options => options.SerializerSettings.ReferenceLoopHandling
= Newtonsoft.Json.ReferenceLoopHandling.Ignore );
But do keep in mind that this still couples your API to your DB models. Maybe that's okay in this project, but if you ever add a column to your DB that you don't want users to see then it becomes an issue. As with anything, you have options.

Creating a Blog Comments and Reply section using ASP.NET MVC 4 (nested collections)

I'm building a Blog Comment and Reply section and I have these three classes mapped to my DB. The first class holds a collection of related comments to an article, the second class holds a collection of related remarks to the comments:
public class Article
{
public int ArticleID { get; set; }
public byte[] Image { get; set; }
public string Title { get; set; }
public string Body { get; set; }
public DateTime DatePublished { get; set; }
public string Author { get; set; }
public CategoryTyp Category { get; set; }
public virtual ICollection<Comment> Comments { get; set; }
}
public class Comment
{
public int CommentID { get; set; }
public int ArticleID { get; set; }
public int CategoryID { get; set; }
public int UserID { get; set; }
public string Description { get; set; }
public DateTime CommentDate { get; set; }
public virtual ICollection<Remark> Remarks { get; set; }
}
public class Remark
{
public int RemarkID { get; set; }
public int CommentID { get; set; }
public int ArticleID { get; set; }
public string RemarkDetail { get; set; }
public DateTime RemarkTime { get; set; }
}
And inside my Controller:
public ActionResult GetArticle(int id)
{
var article = db.Articles.Include("Comments").Where(a => a.ArticleID == id).SingleOrDefault();
return View(article);
}
I understand the basis of eager loading but my questions are:
How do you implement it when you're pulling data from multiple related tables?
What is the best practice of populating it to the View? Once I create a View Model how do I stuff the related collections?
1) With multiple related tables you can have two scenarios:
a) Multiple top level relations: you simply add multiple Include statements (I would suggest using lambda expressions instead of strings for this, to avoid typos).
db.Articles
.Include(a=>a.Comments)
.Include(a=>a.SomethingElse)
.FirstOrDefault(a=>ArticleID==id); // Side note: I would suggest this instead of your Where plus SingleOrDefault
For these scenarios I always use a helper method like this one.
b) Multiple nested related entities:
db.Articles
.Include(a=>a.Comments.Select(c=>c.Remarks)
.FirstOrDefault(a=>ArticleID==id);
2) It's a bit up to you how you pass the data to the views. One best practice I can tell you is that you shouldn't let views lazy load any dependant entities or collections. So your use of Include is correct, but I would even suggest to remove the virtual (deactivate lazy loading) to avoid missing an Include by accident.
Regarding the ViewModels you mention, you are actually not using view models, but your data models. This is OK in most cases, unless you need to format the data somehow or add extra information. Then you would need to create a View Model and map it from the data coming from EF.
Another scenario would be if you used WebAPI or an Ajax Action. In that case, I would suggest to use a DTO (equivalent to a ViewModel) to be able to better control the data returned and its serialization.
One last comment about ViewModels is that if you have heavy entities but you only need a few properties, a good choice is to use Projections, to instruct EF to only load the required properties, instead of the full object.
db.Articles
.Include(a=>a.Comments)
.Select(a=>new ArticleDto { Id = a.ArticleID, Title = a.Title })
.ToListAsync();
This will translate to a "SELECT ArticleID, Title FROM Articles", avoiding returning the article bodies and other stuff that you might not need.
You can chain the relationships with Include. For example:
var article = db.Articles.Include("Comments.Remarks").Where(a => a.ArticleID == id).SingleOrDefault();
I'm not sure what you mean by your second question, though. By issuing this query you already have all the comments and all the remarks for those comments. Therefore, you can access them off of the article instance out of the box:
foreach (var comment in article.Comments)
{
...
foreach (var remark in comment.Remarks)
{
...
}
}
How you handle that with your view model is entirely up to you. You could map the comments/remarks to view models of their own, set them directly on the view model, etc. That's all down to what the needs of your application are, and no one but you can speak to that.

Create View With One-Many Relationship

I have 2 simple models:
public class Country
{
public int ID { get; set; }
public string Name { get; set; }
public virtual ICollection<Region> Region { get; set; }
}
public partial class Region
{
public int ID { get; set; }
public string Name { get; set; }
public int CountryID { get; set; }
public virtual Country Country { get; set; }
}
Is it possible to have a single page to handle the creation of a country whereby the user inputs the country with multiple regions and then only posts to the server?
I've seen an implementation here where you create a custom ViewModel with numbered properties (Region1, Region2, Region3, etc) but it's limiting, any suggestions?
(I know AngularJS can be used to do this however I have no experience in this space as of yet.)
Thanks
Yes its very possible it just depends on how you plan to implement this.
My favourite style of implementing One to Many pages is initially creating the "one" (country) then redirecting to a page with a grid element where users can add the many (regions) to the one. It works well and its a very easy way for both the programmer to create and the user to understand.
As for creating a country with multiple regions in a single post, it could be done but you must think of how the implementation will work.
Sure, this is easy to do. You have defined your data model. Either you use that also as your View Model, or you can create a new model that is a complex object. The methods in your type:
public virtual Country Country { get; set; }
public virtual ICollection<Region> Region { get; set; }
These method being present normally indicates you're using Entity Framework and that these are "related entities" that you can traverse via this "navigation property" at run-time. You can create a Country and populate the Region collection on the fly when you try to use it.
Here is a good example of using a View Model:
What is ViewModel in MVC?
///Example of a Controller method creating a view model to display
[HttpGet]
public ActionResult Index()
{
var user = _userService.Get(User.Identity.Name);
var customerId = GlobalDataManager.GetCustomerId();
if (_error != null)
{
ModelState.AddModelError("", _error);
_error = null;
}
var model = new InboundListModel();
model.Initialize(customerId, user.CompanyId);
foreach (var campaign in model.Campaigns)
{
model.InitializeCallProggress(campaign.Id, _callInfoService.GetCallsCount(campaign.Id));
}
return View(model);
}
This View Model can be anything you want but it does need to be one type. So if you want 2 put 2 types in the ViewModel you just need a new container object:
public class ComplexViewModel
{
public Country Country { get; set; }
public ICollection<Region> Regions { get; set; }
}
Then you just need a way to populate the data like the example above where I call Initialize. This goes out to EF via a DAL project and retrieves the data for the model.

Categories

Resources